Refactoring Part 2: Unread count in Feed instead of Config
This commit is contained in:
@@ -25,8 +25,9 @@
|
||||
@class RSParsedFeed;
|
||||
|
||||
@interface Feed (Ext)
|
||||
+ (Feed*)feedFromRSS:(RSParsedFeed*)obj inContext:(NSManagedObjectContext*)context alreadyRead:(NSArray<NSString*>*)urls unread:(int*)unreadCount;
|
||||
- (NSArray<NSString*>*)alreadyReadURLs;
|
||||
- (void)markAllItemsRead;
|
||||
- (void)markAllItemsUnread;
|
||||
- (void)updateWithRSS:(RSParsedFeed*)obj;
|
||||
- (NSArray<FeedItem*>*)sortedArticles;
|
||||
|
||||
- (int)markAllItemsRead;
|
||||
- (int)markAllItemsUnread;
|
||||
@end
|
||||
|
||||
@@ -27,8 +27,50 @@
|
||||
|
||||
@implementation Feed (Ext)
|
||||
|
||||
+ (FeedItem*)createFeedItemFrom:(RSParsedArticle*)entry inContext:(NSManagedObjectContext*)context {
|
||||
FeedItem *b = [[FeedItem alloc] initWithEntity:FeedItem.entity insertIntoManagedObjectContext:context];
|
||||
/**
|
||||
Replace feed title, subtitle and link (if changed). Also adds new articles and removes old ones.
|
||||
*/
|
||||
- (void)updateWithRSS:(RSParsedFeed*)obj {
|
||||
if (![self.title isEqualToString:obj.title]) self.title = obj.title;
|
||||
if (![self.subtitle isEqualToString:obj.subtitle]) self.subtitle = obj.subtitle;
|
||||
if (![self.link isEqualToString:obj.link]) self.link = obj.link;
|
||||
|
||||
NSMutableSet<NSString*> *urls = [[self.items valueForKeyPath:@"link"] mutableCopy];
|
||||
if ([self addMissingArticles:obj updateLinks:urls]) // will remove links in 'urls' that should be kept
|
||||
[self deleteArticlesWithLink:urls]; // remove old, outdated articles
|
||||
}
|
||||
|
||||
/**
|
||||
Append new articles and increment their sortIndex. Update article counter and unread counter on the way.
|
||||
|
||||
@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 {
|
||||
int latestID = [[self.items valueForKeyPath:@"@max.sortIndex"] intValue];
|
||||
__block int newOnes = 0;
|
||||
[obj.articles enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(RSParsedArticle * _Nonnull article, BOOL * _Nonnull stop) {
|
||||
// reverse enumeration ensures correct article order
|
||||
if ([urls containsObject:article.link]) {
|
||||
[urls removeObject:article.link];
|
||||
} else {
|
||||
newOnes += 1;
|
||||
[self insertArticle:article atIndex:latestID + newOnes];
|
||||
}
|
||||
}];
|
||||
if (newOnes == 0) return NO;
|
||||
self.articleCount += newOnes;
|
||||
self.unreadCount += newOnes; // new articles are by definition unread
|
||||
return YES;
|
||||
}
|
||||
|
||||
/**
|
||||
Create article based on input and insert into core data storage.
|
||||
*/
|
||||
- (void)insertArticle:(RSParsedArticle*)entry atIndex:(int)idx {
|
||||
FeedItem *b = [[FeedItem alloc] initWithEntity:FeedItem.entity insertIntoManagedObjectContext:self.managedObjectContext];
|
||||
b.sortIndex = (int32_t)idx;
|
||||
b.unread = YES;
|
||||
b.guid = entry.guid;
|
||||
b.title = entry.title;
|
||||
b.abstract = entry.abstract;
|
||||
@@ -36,54 +78,72 @@
|
||||
b.author = entry.author;
|
||||
b.link = entry.link;
|
||||
b.published = entry.datePublished;
|
||||
return b;
|
||||
[self addItemsObject:b];
|
||||
}
|
||||
|
||||
+ (Feed*)feedFromRSS:(RSParsedFeed*)obj inContext:(NSManagedObjectContext*)context alreadyRead:(NSArray<NSString*>*)urls unread:(int*)unreadCount {
|
||||
Feed *a = [[Feed alloc] initWithEntity:Feed.entity insertIntoManagedObjectContext:context];
|
||||
a.title = obj.title;
|
||||
a.subtitle = obj.subtitle;
|
||||
a.link = obj.link;
|
||||
for (RSParsedArticle *article in obj.articles) {
|
||||
FeedItem *b = [self createFeedItemFrom:article inContext:context];
|
||||
if ([urls containsObject:b.link]) {
|
||||
b.unread = NO;
|
||||
} else {
|
||||
*unreadCount += 1;
|
||||
}
|
||||
[a addItemsObject:b];
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
- (NSArray<NSString*>*)alreadyReadURLs {
|
||||
if (!self.items || self.items.count == 0) return nil;
|
||||
NSMutableArray<NSString*> *mArr = [NSMutableArray arrayWithCapacity:self.items.count];
|
||||
for (FeedItem *f in self.items) {
|
||||
if (!f.unread) {
|
||||
[mArr addObject:f.link];
|
||||
/**
|
||||
Delete all items where @c link matches one of the URLs in the @c NSSet.
|
||||
*/
|
||||
- (void)deleteArticlesWithLink:(NSMutableSet<NSString*>*)urls {
|
||||
if (!urls || urls.count == 0)
|
||||
return;
|
||||
|
||||
self.articleCount -= (int32_t)urls.count;
|
||||
for (FeedItem *item in self.items) {
|
||||
if ([urls containsObject:item.link]) {
|
||||
[urls removeObject:item.link];
|
||||
if (item.unread)
|
||||
self.unreadCount -= 1;
|
||||
// TODO: keep unread articles?
|
||||
[item.managedObjectContext deleteObject:item];
|
||||
if (urls.count == 0)
|
||||
break;
|
||||
}
|
||||
}
|
||||
return mArr;
|
||||
}
|
||||
|
||||
- (void)markAllItemsRead {
|
||||
[self markAllArticlesRead:YES];
|
||||
/**
|
||||
@return Articles sorted by attribute @c sortIndex with descending order (newest items first).
|
||||
*/
|
||||
- (NSArray<FeedItem*>*)sortedArticles {
|
||||
if (self.items.count == 0)
|
||||
return nil;
|
||||
return [self.items sortedArrayUsingDescriptors:@[[NSSortDescriptor sortDescriptorWithKey:@"sortIndex" ascending:NO]]];
|
||||
}
|
||||
|
||||
- (void)markAllItemsUnread {
|
||||
[self markAllArticlesRead:NO];
|
||||
/**
|
||||
For all articles set @c unread @c = @c NO
|
||||
|
||||
@return Change in unread count. (0 or negative number)
|
||||
*/
|
||||
- (int)markAllItemsRead {
|
||||
return [self markAllArticlesRead:YES];
|
||||
}
|
||||
|
||||
- (void)markAllArticlesRead:(BOOL)readFlag {
|
||||
int count = 0;
|
||||
/**
|
||||
For all articles set @c unread @c = @c YES
|
||||
|
||||
@return Change in unread count. (0 or positive number)
|
||||
*/
|
||||
- (int)markAllItemsUnread {
|
||||
return [self markAllArticlesRead:NO];
|
||||
}
|
||||
|
||||
/**
|
||||
Mark all articles read or unread and update @c unreadCount
|
||||
|
||||
@param readFlag @c YES: mark items read; @c NO: mark items unread
|
||||
*/
|
||||
- (int)markAllArticlesRead:(BOOL)readFlag {
|
||||
for (FeedItem *i in self.items) {
|
||||
if (i.unread == readFlag) {
|
||||
if (i.unread == readFlag)
|
||||
i.unread = !readFlag;
|
||||
++count;
|
||||
}
|
||||
}
|
||||
[self.config markUnread:(readFlag ? -count : +count) ancestorsOnly:NO];
|
||||
int32_t oldCount = self.unreadCount;
|
||||
int32_t newCount = (readFlag ? 0 : self.articleCount);
|
||||
if (self.unreadCount != newCount)
|
||||
self.unreadCount = newCount;
|
||||
return newCount - oldCount;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -33,16 +33,15 @@ typedef enum int16_t {
|
||||
} FeedConfigType;
|
||||
|
||||
@property (getter=typ, setter=setTyp:) FeedConfigType typ;
|
||||
|
||||
- (NSArray<FeedConfig*>*)sortedChildren;
|
||||
- (NSIndexPath*)indexPath;
|
||||
- (void)markUnread:(int)count ancestorsOnly:(BOOL)flag;
|
||||
- (void)calculateAndSetScheduled;
|
||||
- (BOOL)iterateSorted:(BOOL)ordered overDescendantFeeds:(void(^)(Feed*,BOOL*))block;
|
||||
|
||||
- (void)setEtag:(NSString*)etag modified:(NSString*)modified;
|
||||
// Handle children and parents
|
||||
- (NSString*)indexPathString;
|
||||
- (NSMutableArray<FeedConfig*>*)allParents;
|
||||
- (BOOL)iterateSorted:(BOOL)ordered overDescendantFeeds:(void(^)(Feed *feed, BOOL* cancel))block;
|
||||
// Update feed and meta
|
||||
- (void)updateRSSFeed:(RSParsedFeed*)obj;
|
||||
|
||||
- (void)setEtag:(NSString*)etag modified:(NSString*)modified;
|
||||
- (void)calculateAndSetScheduled;
|
||||
// Printing
|
||||
- (NSString*)readableRefreshString;
|
||||
- (NSString*)readableDescription;
|
||||
@end
|
||||
|
||||
@@ -31,76 +31,40 @@
|
||||
/// Enum type setter see @c FeedConfigType
|
||||
- (void)setTyp:(FeedConfigType)typ { self.type = typ; }
|
||||
|
||||
/**
|
||||
Sorted children array based on sort order provided in feed settings.
|
||||
|
||||
@return Sorted array of @c FeedConfig items.
|
||||
*/
|
||||
#pragma mark - Handle Children And Parents -
|
||||
|
||||
|
||||
/// @return IndexPath as semicolon separated string for sorted children starting with root index.
|
||||
- (NSString*)indexPathString {
|
||||
if (self.parent == nil)
|
||||
return [NSString stringWithFormat:@"%d", self.sortIndex];
|
||||
return [[self.parent indexPathString] stringByAppendingFormat:@".%d", self.sortIndex];
|
||||
}
|
||||
|
||||
/// @return Children sorted by attribute @c sortIndex (same order as in preferences).
|
||||
- (NSArray<FeedConfig*>*)sortedChildren {
|
||||
if (self.children.count == 0)
|
||||
return nil;
|
||||
return [self.children sortedArrayUsingDescriptors:@[[NSSortDescriptor sortDescriptorWithKey:@"sortIndex" ascending:YES]]];
|
||||
}
|
||||
|
||||
/// IndexPath for sorted children starting with root index.
|
||||
- (NSIndexPath*)indexPath {
|
||||
/// @return @c NSArray of all ancestors: First object is root. Last object is the @c FeedConfig that executed the command.
|
||||
- (NSMutableArray<FeedConfig*>*)allParents {
|
||||
if (self.parent == nil)
|
||||
return [NSIndexPath indexPathWithIndex:(NSUInteger)self.sortIndex];
|
||||
return [[self.parent indexPath] indexPathByAddingIndex:(NSUInteger)self.sortIndex];
|
||||
return [NSMutableArray arrayWithObject:self];
|
||||
NSMutableArray *arr = [self.parent allParents];
|
||||
[arr addObject:self];
|
||||
return arr;
|
||||
}
|
||||
|
||||
/**
|
||||
Change unread counter for all parents recursively. Result will never be negative.
|
||||
Iterate over all descenden feeds.
|
||||
|
||||
@param count If negative, mark items read.
|
||||
@param ordered If @c YES items are executed in the same order they are listed in the menu. Pass @n NO for a speed-up.
|
||||
@param block Set @c cancel to @c YES to stop execution of further descendants.
|
||||
@return @c NO if execution was stopped with @c cancel @c = @c YES in @c block.
|
||||
*/
|
||||
- (void)markUnread:(int)count ancestorsOnly:(BOOL)flag {
|
||||
FeedConfig *par = (flag ? self.parent : self);
|
||||
while (par) {
|
||||
[self.managedObjectContext refreshObject:par mergeChanges:YES];
|
||||
par.unreadCount += count;
|
||||
NSAssert(par.unreadCount >= 0, @"ERROR ancestorsMarkUnread: Count should never be negative.");
|
||||
par = par.parent;
|
||||
}
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:kNotificationTotalUnreadCountChanged
|
||||
object:[NSNumber numberWithInt:count]];
|
||||
}
|
||||
|
||||
/// @return Time interval respecting the selected unit. E.g., returns @c 180 for @c '3m'
|
||||
- (NSTimeInterval)timeInterval {
|
||||
static const int unit[] = {1, 60, 3600, 86400, 604800}; // smhdw
|
||||
return self.refreshNum * unit[self.refreshUnit % 5];
|
||||
}
|
||||
|
||||
/// Calculate date from @c refreshNum and @c refreshUnit and set as next scheduled feed update.
|
||||
- (void)calculateAndSetScheduled {
|
||||
self.scheduled = [[NSDate date] dateByAddingTimeInterval:[self timeInterval]];
|
||||
}
|
||||
|
||||
/// Update FeedMeta or create new one if needed.
|
||||
- (void)setEtag:(NSString*)etag modified:(NSString*)modified {
|
||||
// TODO: move to separate function and add icon download
|
||||
if (!self.meta) {
|
||||
self.meta = [[FeedMeta alloc] initWithEntity:FeedMeta.entity insertIntoManagedObjectContext:self.managedObjectContext];
|
||||
}
|
||||
self.meta.httpEtag = etag;
|
||||
self.meta.httpModified = modified;
|
||||
}
|
||||
|
||||
/// Delete any existing feed object and parse new one. Read state will be copied.
|
||||
- (void)updateRSSFeed:(RSParsedFeed*)obj {
|
||||
NSArray<NSString*> *readURLs = [self.feed alreadyReadURLs];
|
||||
int unreadBefore = self.unreadCount;
|
||||
int unreadAfter = 0;
|
||||
if (self.feed)
|
||||
[self.managedObjectContext deleteObject:(NSManagedObject*)self.feed];
|
||||
if (obj) {
|
||||
// TODO: update and dont re-create each time
|
||||
self.feed = [Feed feedFromRSS:obj inContext:self.managedObjectContext alreadyRead:readURLs unread:&unreadAfter];
|
||||
}
|
||||
[self markUnread:(unreadAfter - unreadBefore) ancestorsOnly:NO];
|
||||
}
|
||||
|
||||
- (BOOL)iterateSorted:(BOOL)ordered overDescendantFeeds:(void(^)(Feed*,BOOL*))block {
|
||||
if (self.feed) {
|
||||
BOOL stopEarly = NO;
|
||||
@@ -115,8 +79,46 @@
|
||||
return YES;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Update Feed And Meta -
|
||||
|
||||
|
||||
/// Delete any existing feed object and parse new one. Read state will be copied.
|
||||
- (void)updateRSSFeed:(RSParsedFeed*)obj {
|
||||
if (!self.feed) {
|
||||
self.feed = [[Feed alloc] initWithEntity:Feed.entity insertIntoManagedObjectContext:self.managedObjectContext];
|
||||
self.feed.indexPath = [self indexPathString];
|
||||
}
|
||||
int32_t unreadBefore = self.feed.unreadCount;
|
||||
[self.feed updateWithRSS:obj];
|
||||
NSNumber *cDiff = [NSNumber numberWithInteger:self.feed.unreadCount - unreadBefore];
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:kNotificationTotalUnreadCountChanged object:cDiff];
|
||||
}
|
||||
|
||||
/// Update FeedMeta or create new one if needed.
|
||||
- (void)setEtag:(NSString*)etag modified:(NSString*)modified {
|
||||
if (!self.meta) {
|
||||
self.meta = [[FeedMeta alloc] initWithEntity:FeedMeta.entity insertIntoManagedObjectContext:self.managedObjectContext];
|
||||
}
|
||||
if (![self.meta.httpEtag isEqualToString:etag]) self.meta.httpEtag = etag;
|
||||
if (![self.meta.httpModified isEqualToString:modified]) self.meta.httpModified = modified;
|
||||
}
|
||||
|
||||
/// Calculate date from @c refreshNum and @c refreshUnit and set as next scheduled feed update.
|
||||
- (void)calculateAndSetScheduled {
|
||||
self.scheduled = [[NSDate date] dateByAddingTimeInterval:[self timeInterval]];
|
||||
}
|
||||
|
||||
/// @return Time interval respecting the selected unit. E.g., returns @c 180 for @c '3m'
|
||||
- (NSTimeInterval)timeInterval {
|
||||
static const int unit[] = {1, 60, 3600, 86400, 604800}; // smhdw
|
||||
return self.refreshNum * unit[self.refreshUnit % 5];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Printing -
|
||||
|
||||
|
||||
/// @return Formatted string for update interval ( e.g., @c 30m or @c 12h )
|
||||
- (NSString*)readableRefreshString {
|
||||
return [NSString stringWithFormat:@"%d%c", self.refreshNum, [@"smhdw" characterAtIndex:self.refreshUnit % 5]];
|
||||
|
||||
Reference in New Issue
Block a user