4 Commits

Author SHA1 Message Date
relikd
6192f76605 Fix update for feeds where all articles have the same URL. Closes #3 2019-03-15 00:09:00 +01:00
relikd
3ace169549 For tooltip use article.body if article.abstract is empty 2019-03-15 00:08:19 +01:00
relikd
039c7bc734 Fix: 'Update all feeds' unread count during update 2019-03-10 12:16:32 +01:00
relikd
0c9b128cfe changelog 2019-03-07 18:48:12 +01:00
8 changed files with 99 additions and 35 deletions

57
CHANGELOG.md Normal file
View File

@@ -0,0 +1,57 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project does NOT adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [0.9.3] - 2019-03-14
### Added
- Changelog
- UI: Show body tag in article tooltip if abstract tag is empty
### Fixed
- 'Update all feeds' will shows unread items count properly during update
- Fixed update for feeds where all article URLs point to the same resource (issue: #3)
## [0.9.2] - 2019-03-07
### Added
- Limit number of articles that are displayed in feed menu (issue: #2)
### Fixed
- Cmd+Q in preferences will close the window instead of quitting the application
- Crash when libxml2 encountered and set an error
- libxml2 will ignore lower ascii characters (0x000x1F)
## [0.9.1] - 2019-02-14
### Added
- Mark single article as un/read (hold down option key and click on article)
### Fixed
- Mouse click on 'Done' button, while entering a new feed URL, will start download properly
- Use guid url if link is not set (issue: #1)
- Issue with feeds not being detected if XML tags start after 4kb
- Support uppercase schemes (e.g., 'FEED:')
- UI: Hide 'Next update in -25yrs'
- UI: Show alert after click on 'Fix Cache'
### Changed
- Auto increment build number
- Removed static images for group icon, default feed icon, and warning icon
- Remove html tags from abstract on save (not on display)
## [0.9] - 2019-02-11
Initial release
[Unreleased]: https://github.com/relikd/baRSS/compare/v0.9.2...HEAD
[0.9.3]: https://github.com/relikd/baRSS/compare/v0.9.2...v0.9.3
[0.9.2]: https://github.com/relikd/baRSS/compare/v0.9.1...v0.9.2
[0.9.1]: https://github.com/relikd/baRSS/compare/v0.9...v0.9.1
[0.9]: https://github.com/relikd/baRSS/compare/e1f36514a8aa2d5fb9a575b6eb19adc2ce4a04d9...v0.9

View File

@@ -106,6 +106,7 @@
546FC4462118A8E6007CC3A3 /* Preferences.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = Preferences.xib; sourceTree = "<group>"; }; 546FC4462118A8E6007CC3A3 /* Preferences.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = Preferences.xib; sourceTree = "<group>"; };
5477D34C21233C62002BA27F /* FeedGroup+Ext.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "FeedGroup+Ext.h"; sourceTree = "<group>"; }; 5477D34C21233C62002BA27F /* FeedGroup+Ext.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "FeedGroup+Ext.h"; sourceTree = "<group>"; };
5477D34D21233C62002BA27F /* FeedGroup+Ext.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "FeedGroup+Ext.m"; sourceTree = "<group>"; }; 5477D34D21233C62002BA27F /* FeedGroup+Ext.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "FeedGroup+Ext.m"; sourceTree = "<group>"; };
54892F1D2235285700271CBA /* CHANGELOG.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = CHANGELOG.md; sourceTree = "<group>"; };
5496B50F214D6275003ED4ED /* UserPrefs.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = UserPrefs.h; sourceTree = "<group>"; }; 5496B50F214D6275003ED4ED /* UserPrefs.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = UserPrefs.h; sourceTree = "<group>"; };
5496B510214D6275003ED4ED /* UserPrefs.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = UserPrefs.m; sourceTree = "<group>"; }; 5496B510214D6275003ED4ED /* UserPrefs.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = UserPrefs.m; sourceTree = "<group>"; };
54A07A7D220E04CF00082C51 /* NSFetchRequest+Ext.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSFetchRequest+Ext.h"; sourceTree = "<group>"; }; 54A07A7D220E04CF00082C51 /* NSFetchRequest+Ext.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSFetchRequest+Ext.h"; sourceTree = "<group>"; };
@@ -251,6 +252,7 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
540CD14821C094A2004AB594 /* README.md */, 540CD14821C094A2004AB594 /* README.md */,
54892F1D2235285700271CBA /* CHANGELOG.md */,
54ACC27E21061B3B0020715F /* baRSS */, 54ACC27E21061B3B0020715F /* baRSS */,
54CC042D2162532800A48795 /* baRSS-Helper */, 54CC042D2162532800A48795 /* baRSS-Helper */,
54ACC27D21061B3B0020715F /* Products */, 54ACC27D21061B3B0020715F /* Products */,

View File

@@ -92,9 +92,9 @@
self.group.name = obj.title; self.group.name = obj.title;
// Add and remove articles // Add and remove articles
NSMutableSet<NSString*> *urls = [[self.articles valueForKeyPath:@"link"] mutableCopy]; NSMutableSet<FeedArticle*> *oldSet = [self.articles mutableCopy];
NSInteger diff = [self addMissingArticles:obj updateLinks:urls]; // will remove links in 'urls' that should be kept NSInteger diff = [self addMissingArticles:obj withOldSet:oldSet]; // will remove items that should be kept
diff -= [self deleteArticlesWithLink:urls]; // remove old, outdated articles diff -= [self deleteArticlesWithOldSet:oldSet]; // remove old, outdated articles
// Get new total article count and post unread-count-change notification // Get new total article count and post unread-count-change notification
if (flag && diff != 0) { if (flag && diff != 0) {
[[NSNotificationCenter defaultCenter] postNotificationName:kNotificationTotalUnreadCountChanged object:@(diff)]; [[NSNotificationCenter defaultCenter] postNotificationName:kNotificationTotalUnreadCountChanged object:@(diff)];
@@ -108,9 +108,10 @@
New articles should be in ascending order without any gaps in between. 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. 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 oldSet Input will be used to identify new articles.
Output contains articles that aren't present in the feed anymore and should be deleted.
*/ */
- (NSInteger)addMissingArticles:(RSParsedFeed*)obj updateLinks:(NSMutableSet<NSString*>*)urls { - (NSInteger)addMissingArticles:(RSParsedFeed*)obj withOldSet:(NSMutableSet<FeedArticle*>*)oldSet {
NSInteger newOnes = 0; NSInteger newOnes = 0;
int32_t currentIndex = [[self.articles valueForKeyPath:@"@min.sortIndex"] intValue]; int32_t currentIndex = [[self.articles valueForKeyPath:@"@min.sortIndex"] intValue];
FeedArticle *lastInserted = nil; FeedArticle *lastInserted = nil;
@@ -118,10 +119,10 @@
for (RSParsedArticle *article in [obj.articles reverseObjectEnumerator]) { for (RSParsedArticle *article in [obj.articles reverseObjectEnumerator]) {
// reverse enumeration ensures correct article order // reverse enumeration ensures correct article order
if ([urls containsObject:article.link]) { FeedArticle *storedArticle = [self findArticle:article inSet:oldSet];
[urls removeObject:article.link]; if (storedArticle) {
FeedArticle *storedArticle = [self findArticleWithLink:article.link]; // TODO: use two synced arrays? [oldSet removeObject:storedArticle];
if (storedArticle && storedArticle.sortIndex != currentIndex) { if (storedArticle.sortIndex != currentIndex) {
storedArticle.sortIndex = currentIndex; storedArticle.sortIndex = currentIndex;
} }
hasGapBetweenNewArticles = YES; hasGapBetweenNewArticles = YES;
@@ -146,26 +147,19 @@
} }
/** /**
Delete all items where @c link matches one of the URLs in the @c NSSet. Delete all articles from core data, that are still in the oldSet.
*/ */
- (NSUInteger)deleteArticlesWithLink:(NSMutableSet<NSString*>*)urls { - (NSUInteger)deleteArticlesWithOldSet:(NSMutableSet<FeedArticle*>*)oldSet {
if (!urls || urls.count == 0) if (!oldSet || oldSet.count == 0)
return 0; return 0;
NSUInteger c = 0; NSUInteger c = 0;
for (FeedArticle *fa in self.articles) { for (FeedArticle *fa in oldSet) {
if ([urls containsObject:fa.link]) { if (fa.unread) ++c;
[urls removeObject:fa.link]; // TODO: keep unread articles?
if (fa.unread) ++c; [self.managedObjectContext deleteObject:fa];
// TODO: keep unread articles?
[self.managedObjectContext deleteObject:fa];
if (urls.count == 0)
break;
}
}
NSSet<FeedArticle*> *delArticles = [self.managedObjectContext deletedObjects];
if (delArticles.count > 0) {
[self removeArticles:delArticles];
} }
if (oldSet.count > 0)
[self removeArticles:oldSet];
return c; return c;
} }
@@ -183,12 +177,18 @@
} }
/** /**
Iterate over all Articles and return the one where @c .link matches. Or @c nil if no matching article found. Iterate over oldSet and return the one where @c link and @c guid matches. Or @c nil if no matching article found.
*/ */
- (FeedArticle*)findArticleWithLink:(NSString*)url { - (FeedArticle*)findArticle:(RSParsedArticle*)article inSet:(NSSet<FeedArticle*>*)oldSet {
for (FeedArticle *a in self.articles) { NSString *searchLink = article.link;
if ([a.link isEqualToString:url]) NSString *searchGuid = article.guid;
return a; BOOL linkIsNil = (searchLink == nil);
BOOL guidIsNil = (searchGuid == nil);
for (FeedArticle *old in oldSet) {
if ((linkIsNil && old.link == nil) || [old.link isEqualToString:searchLink]) {
if ((guidIsNil && old.guid == nil) || [old.guid isEqualToString:searchGuid])
return old;
}
} }
return nil; return nil;
} }

View File

@@ -67,7 +67,7 @@
item.title = [self shortArticleName]; item.title = [self shortArticleName];
item.enabled = (self.link.length > 0); item.enabled = (self.link.length > 0);
item.state = (self.unread && [UserPrefs defaultYES:@"feedTickMark"] ? NSControlStateValueOn : NSControlStateValueOff); item.state = (self.unread && [UserPrefs defaultYES:@"feedTickMark"] ? NSControlStateValueOn : NSControlStateValueOff);
item.toolTip = self.abstract; item.toolTip = (self.abstract ? self.abstract : self.body); // fall back to body (html)
item.representedObject = self.objectID; item.representedObject = self.objectID;
item.target = [self class]; item.target = [self class];
item.action = @selector(didClickOnMenuItem:); item.action = @selector(didClickOnMenuItem:);

View File

@@ -34,7 +34,9 @@
self.errorCount = 0; self.errorCount = 0;
int16_t n = self.errorCount + 1; // always increment errorCount (can be used to indicate bad feeds) int16_t n = self.errorCount + 1; // always increment errorCount (can be used to indicate bad feeds)
// TODO: remove logging // TODO: remove logging
#ifdef DEBUG
NSLog(@"ERROR: Feed download failed: %@ (errorCount: %d)", self.url, n); NSLog(@"ERROR: Feed download failed: %@ (errorCount: %d)", self.url, n);
#endif
if ([self.scheduled timeIntervalSinceNow] > 30) // forced, early update. Scheduled is still in the futute. if ([self.scheduled timeIntervalSinceNow] > 30) // forced, early update. Scheduled is still in the futute.
return; // Keep error counter low. Not enough time has passed (e.g., temporary server outage) return; // Keep error counter low. Not enough time has passed (e.g., temporary server outage)
NSTimeInterval retryWaitTime = pow(2, (n > 13 ? 13 : n)) * 60; // 2^N (between: 2 minutes and 5.7 days) NSTimeInterval retryWaitTime = pow(2, (n > 13 ? 13 : n)) * 60; // 2^N (between: 2 minutes and 5.7 days)

View File

@@ -123,7 +123,9 @@ static BOOL _nextUpdateIsForced = NO;
Called when schedule timer runs out (earliest @c .schedule date). Or if forced by user request. Called when schedule timer runs out (earliest @c .schedule date). Or if forced by user request.
*/ */
+ (void)updateTimerCallback { + (void)updateTimerCallback {
#ifdef DEBUG
NSLog(@"fired"); NSLog(@"fired");
#endif
BOOL updateAll = _nextUpdateIsForced; BOOL updateAll = _nextUpdateIsForced;
_nextUpdateIsForced = NO; _nextUpdateIsForced = NO;
@@ -303,7 +305,7 @@ static BOOL _nextUpdateIsForced = NO;
needsNotification = YES; needsNotification = YES;
} }
} }
[StoreCoordinator saveContext:moc andParent:NO]; [StoreCoordinator saveContext:moc andParent:YES];
if (needsNotification) if (needsNotification)
[[NSNotificationCenter defaultCenter] postNotificationName:kNotificationFeedUpdated object:oid]; [[NSNotificationCenter defaultCenter] postNotificationName:kNotificationFeedUpdated object:oid];
if (block) block(success); if (block) block(success);
@@ -400,7 +402,7 @@ static BOOL _nextUpdateIsForced = NO;
[self downloadFavicon:faviconURL finished:^(NSImage *img) { [self downloadFavicon:faviconURL finished:^(NSImage *img) {
Feed *f = [moc objectWithID:oid]; Feed *f = [moc objectWithID:oid];
if (f && [f setIconImage:img]) { if (f && [f setIconImage:img]) {
[StoreCoordinator saveContext:moc andParent:NO]; [StoreCoordinator saveContext:moc andParent:YES];
[[NSNotificationCenter defaultCenter] postNotificationName:kNotificationFeedIconUpdated object:oid]; [[NSNotificationCenter defaultCenter] postNotificationName:kNotificationFeedIconUpdated object:oid];
} }
if (block) block(); if (block) block();

View File

@@ -17,7 +17,7 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>0.9.2</string> <string>0.9.3</string>
<key>CFBundleURLTypes</key> <key>CFBundleURLTypes</key>
<array> <array>
<dict> <dict>
@@ -32,7 +32,7 @@
</dict> </dict>
</array> </array>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>1122</string> <string>1153</string>
<key>LSMinimumSystemVersion</key> <key>LSMinimumSystemVersion</key>
<string>$(MACOSX_DEPLOYMENT_TARGET)</string> <string>$(MACOSX_DEPLOYMENT_TARGET)</string>
<key>LSUIElement</key> <key>LSUIElement</key>

View File

@@ -154,6 +154,7 @@
[self.unreadMap updateAllCounts:updated forPath:feed.indexPath]; [self.unreadMap updateAllCounts:updated forPath:feed.indexPath];
// 2. rebuild articles menu if it is open // 2. rebuild articles menu if it is open
if (item.submenu.isFeedMenu) { // menu item is visible if (item.submenu.isFeedMenu) { // menu item is visible
item.image = [feed iconImage16];
item.enabled = (feed.articles.count > 0); item.enabled = (feed.articles.count > 0);
if (item.submenu.numberOfItems > 0) { // replace articles menu if (item.submenu.numberOfItems > 0) { // replace articles menu
[item.submenu removeAllItems]; [item.submenu removeAllItems];