Refactoring Part 2: Unread count in Feed instead of Config

This commit is contained in:
relikd
2018-12-04 01:01:28 +01:00
parent 6223d1a169
commit ae4700faca
19 changed files with 560 additions and 445 deletions

View File

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

View File

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

View File

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

View File

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