diff --git a/app/src/main/java/com/capyreader/app/ui/articles/ArticleLayout.kt b/app/src/main/java/com/capyreader/app/ui/articles/ArticleLayout.kt index 53ae9353..002703b9 100644 --- a/app/src/main/java/com/capyreader/app/ui/articles/ArticleLayout.kt +++ b/app/src/main/java/com/capyreader/app/ui/articles/ArticleLayout.kt @@ -299,7 +299,7 @@ fun ArticleLayout( onFeedAdded = { onFeedAdded(it) }, onNavigateToSettings = onNavigateToSettings, onFilterSelect = { - if (!filter.hasArticlesSelected()) { + if (!filter.hasArticleSelected()) { openNextList { onSelectArticleFilter() } } else { closeDrawer() diff --git a/app/src/main/java/com/capyreader/app/ui/articles/ArticleScreenViewModel.kt b/app/src/main/java/com/capyreader/app/ui/articles/ArticleScreenViewModel.kt index db717715..ea95819a 100644 --- a/app/src/main/java/com/capyreader/app/ui/articles/ArticleScreenViewModel.kt +++ b/app/src/main/java/com/capyreader/app/ui/articles/ArticleScreenViewModel.kt @@ -223,7 +223,7 @@ class ArticleScreenViewModel( refreshJob?.cancel() refreshJob = viewModelScope.launch(Dispatchers.IO) { - account.refresh().onFailure { throwable -> + account.refresh(latestFilter).onFailure { throwable -> if (throwable is UnauthorizedError && _showUnauthorizedMessage == UnauthorizedMessageState.HIDE) { _showUnauthorizedMessage = UnauthorizedMessageState.SHOW } diff --git a/app/src/main/java/com/capyreader/app/ui/articles/FeedList.kt b/app/src/main/java/com/capyreader/app/ui/articles/FeedList.kt index 70108ce4..6bd0c623 100644 --- a/app/src/main/java/com/capyreader/app/ui/articles/FeedList.kt +++ b/app/src/main/java/com/capyreader/app/ui/articles/FeedList.kt @@ -102,7 +102,7 @@ fun FeedList( ) }, badge = { CountBadge(count = statusCount) }, - selected = filter.hasArticlesSelected(), + selected = filter.hasArticleSelected(), onClick = { onFilterSelect() } diff --git a/capy/src/main/java/com/jocmp/capy/ArticleFilter.kt b/capy/src/main/java/com/jocmp/capy/ArticleFilter.kt index bef25381..20611ce5 100644 --- a/capy/src/main/java/com/jocmp/capy/ArticleFilter.kt +++ b/capy/src/main/java/com/jocmp/capy/ArticleFilter.kt @@ -12,7 +12,7 @@ sealed class ArticleFilter(open val status: ArticleStatus) { return this is Feeds && this.feedID == feed.id && this.folderTitle.orEmpty() == feed.folderName } - fun hasArticlesSelected(): Boolean { + fun hasArticleSelected(): Boolean { return this is Articles } diff --git a/capy/src/main/java/com/jocmp/capy/accounts/feedbin/FeedbinAccountDelegate.kt b/capy/src/main/java/com/jocmp/capy/accounts/feedbin/FeedbinAccountDelegate.kt index 016076e8..953f1300 100644 --- a/capy/src/main/java/com/jocmp/capy/accounts/feedbin/FeedbinAccountDelegate.kt +++ b/capy/src/main/java/com/jocmp/capy/accounts/feedbin/FeedbinAccountDelegate.kt @@ -44,7 +44,7 @@ internal class FeedbinAccountDelegate( override suspend fun refresh(filter: ArticleFilter, cutoffDate: ZonedDateTime?): Result { return try { - val since = articleRecords.maxUpdatedAt().toString() + val since = articleRecords.maxArrivedAt().toString() refreshFeeds() refreshTaggings() @@ -190,7 +190,7 @@ internal class FeedbinAccountDelegate( Unit } - private suspend fun refreshArticles(since: String = articleRecords.maxUpdatedAt().toString()) { + private suspend fun refreshArticles(since: String = articleRecords.maxArrivedAt().toString()) { refreshStarredEntries() refreshUnreadEntries() refreshAllArticles(since = since) diff --git a/capy/src/main/java/com/jocmp/capy/accounts/reader/ReaderAccountDelegate.kt b/capy/src/main/java/com/jocmp/capy/accounts/reader/ReaderAccountDelegate.kt index 47e6c08d..fbfbf55d 100644 --- a/capy/src/main/java/com/jocmp/capy/accounts/reader/ReaderAccountDelegate.kt +++ b/capy/src/main/java/com/jocmp/capy/accounts/reader/ReaderAccountDelegate.kt @@ -2,6 +2,7 @@ package com.jocmp.capy.accounts.reader import com.jocmp.capy.AccountDelegate import com.jocmp.capy.ArticleFilter +import com.jocmp.capy.ArticleStatus import com.jocmp.capy.Feed import com.jocmp.capy.accounts.AddFeedResult import com.jocmp.capy.accounts.Source @@ -26,6 +27,7 @@ import com.jocmp.readerclient.Stream import com.jocmp.readerclient.Subscription import com.jocmp.readerclient.SubscriptionEditAction import com.jocmp.readerclient.SubscriptionQuickAddResult +import com.jocmp.readerclient.buildHexID import com.jocmp.readerclient.ext.editSubscription import com.jocmp.readerclient.ext.streamItemsIDs import kotlinx.coroutines.coroutineScope @@ -48,31 +50,30 @@ internal class ReaderAccountDelegate( override suspend fun refresh(filter: ArticleFilter, cutoffDate: ZonedDateTime?): Result { return withErrorHandling { - val since = articleRecords.maxUpdatedAt().toEpochSecond() - - refreshFeeds() - refreshArticles(since = since) + refreshTopLevelArticles(filter) + val stream = filter.toStream(source) + refreshArticles(stream = stream, since = since(stream)) } } override suspend fun markRead(articleIDs: List): Result { val results = articleIDs.chunked(MAX_CREATE_UNREAD_LIMIT).map { batchIDs -> - editTag(ids = batchIDs, addTag = Stream.READ) + editTag(ids = batchIDs, addTag = Stream.Read()) } return results.firstOrNull { it.isFailure } ?: Result.success(Unit) } override suspend fun markUnread(articleIDs: List): Result { - return editTag(ids = articleIDs, removeTag = Stream.READ) + return editTag(ids = articleIDs, removeTag = Stream.Read()) } override suspend fun addStar(articleIDs: List): Result { - return editTag(ids = articleIDs, addTag = Stream.STARRED) + return editTag(ids = articleIDs, addTag = Stream.Starred()) } override suspend fun removeStar(articleIDs: List): Result { - return editTag(ids = articleIDs, removeTag = Stream.STARRED) + return editTag(ids = articleIDs, removeTag = Stream.Starred()) } override suspend fun addFeed( @@ -98,7 +99,7 @@ internal class ReaderAccountDelegate( if (feed != null) { coroutineScope { - launch { refreshArticles() } + launch { refreshArticles(stream = Stream.ReadingList()) } } AddFeedResult.Success(feed) @@ -199,6 +200,22 @@ internal class ReaderAccountDelegate( } } + private suspend fun refreshTopLevelArticles(filter: ArticleFilter) { + if (!filter.hasArticleSelected()) { + return + } + + refreshFeeds() + + if (filter.status != ArticleStatus.UNREAD) { + refreshStarredItems() + } + + if (filter.status != ArticleStatus.STARRED) { + refreshUnreadItems() + } + } + private fun upsertTaggings(subscription: Subscription) { subscription.categories.forEach { category -> database.taggingsQueries.upsert( @@ -238,19 +255,21 @@ internal class ReaderAccountDelegate( } private suspend fun refreshArticles( - since: Long = articleRecords.maxUpdatedAt().toEpochSecond() + stream: Stream, + since: Long? = null ) { - refreshStarredItems() - refreshUnreadItems() - refreshAllArticles(since = since) + fetchPaginatedArticles( + since = since, + stream = stream + ) fetchMissingArticles() } private suspend fun refreshUnreadItems() { withResult( googleReader.streamItemsIDs( - stream = Stream.READING_LIST, - excludedStream = Stream.READ + stream = Stream.ReadingList(), + excludedStream = Stream.Read() ) ) { result -> articleRecords.markAllUnread(articleIDs = result.itemRefs.map { it.hexID }) @@ -258,7 +277,7 @@ internal class ReaderAccountDelegate( } private suspend fun refreshStarredItems() { - withResult(googleReader.streamItemsIDs(stream = Stream.STARRED)) { result -> + withResult(googleReader.streamItemsIDs(stream = Stream.Starred())) { result -> articleRecords.markAllStarred(articleIDs = result.itemRefs.map { it.hexID }) } } @@ -272,7 +291,7 @@ internal class ReaderAccountDelegate( val response = withPostToken { googleReader.streamItemsContents( postToken = postToken.get(), - ids = chunkedIDs + ids = chunkedIDs.map { buildHexID(it) } ) } @@ -284,14 +303,7 @@ internal class ReaderAccountDelegate( } } - private suspend fun refreshAllArticles(since: Long) { - fetchPaginatedItems( - since = since, - stream = Stream.READING_LIST - ) - } - - private suspend fun fetchPaginatedItems( + private suspend fun fetchPaginatedArticles( since: Long? = null, stream: Stream, continuation: String? = null, @@ -317,7 +329,7 @@ internal class ReaderAccountDelegate( val nextContinuation = result.continuation ?: return - fetchPaginatedItems( + fetchPaginatedArticles( since = since, stream = stream, continuation = nextContinuation @@ -358,7 +370,7 @@ internal class ReaderAccountDelegate( articleRecords.updateStatus( articleID = item.hexID, updatedAt = updated, - read = item.read + read = item.read, ) } } @@ -410,7 +422,6 @@ internal class ReaderAccountDelegate( postToken.set(googleReader.token().body()) } catch (exception: IOException) { CapyLog.error(tag("post_token"), exception) - // continue } } @@ -422,6 +433,14 @@ internal class ReaderAccountDelegate( return "${subscription.id}:${category.id}" } + private fun since(stream: Stream): Long? { + return if (stream.isStateStream) { + articleRecords.maxArrivedAt().toEpochSecond() + } else { + null + } + } + companion object { const val MAX_PAGINATED_ITEM_LIMIT = 100 @@ -446,6 +465,25 @@ private val SubscriptionQuickAddResult.toSubscription: Subscription? ) } +private fun ArticleFilter.toStream(source: Source): Stream { + return when (this) { + is ArticleFilter.Articles -> Stream.Read() + is ArticleFilter.Feeds -> Stream.Feed(feedID) + is ArticleFilter.Folders -> folderStream(this, source) + } +} + +/** + * Default to reading list for folders since Miniflux doesn't support label lookups + */ +private fun folderStream(filter: ArticleFilter.Folders, source: Source): Stream { + return if (source == Source.FRESHRSS) { + Stream.Label(filter.folderTitle) + } else { + Stream.ReadingList() + } +} + private fun userLabel(title: String): String { return "user/-/label/${title}" } diff --git a/capy/src/main/java/com/jocmp/capy/persistence/ArticleRecords.kt b/capy/src/main/java/com/jocmp/capy/persistence/ArticleRecords.kt index 04badb04..28d4449d 100644 --- a/capy/src/main/java/com/jocmp/capy/persistence/ArticleRecords.kt +++ b/capy/src/main/java/com/jocmp/capy/persistence/ArticleRecords.kt @@ -178,7 +178,7 @@ internal class ArticleRecords internal constructor( } /** Date in UTC */ - fun maxUpdatedAt(): ZonedDateTime { + fun maxArrivedAt(): ZonedDateTime { val max = database.articlesQueries.lastUpdatedAt().executeAsOne().MAX max ?: return cutoffDate() diff --git a/capy/src/test/java/com/jocmp/capy/AccountTest.kt b/capy/src/test/java/com/jocmp/capy/AccountTest.kt index 69b114d5..52261a1d 100644 --- a/capy/src/test/java/com/jocmp/capy/AccountTest.kt +++ b/capy/src/test/java/com/jocmp/capy/AccountTest.kt @@ -28,7 +28,7 @@ class AccountTest { @Before fun setup() { account = AccountFixture.create(parentFolder = folder) - coEvery { account.delegate.refresh(any()) }.returns(Result.success(Unit)) + coEvery { account.delegate.refresh(any(), any()) }.returns(Result.success(Unit)) } @Test diff --git a/capy/src/test/java/com/jocmp/capy/OPMLFileTest.kt b/capy/src/test/java/com/jocmp/capy/OPMLFileTest.kt index 7aaa0394..ac568b39 100644 --- a/capy/src/test/java/com/jocmp/capy/OPMLFileTest.kt +++ b/capy/src/test/java/com/jocmp/capy/OPMLFileTest.kt @@ -5,7 +5,7 @@ import com.jocmp.capy.accounts.LocalAccountDelegate import com.jocmp.capy.db.Database import com.jocmp.capy.fixtures.AccountFixture import com.jocmp.capy.fixtures.FeedFixture -import com.jocmp.capy.fixtures.TagFixture +import com.jocmp.capy.fixtures.FolderFixture import io.mockk.mockk import kotlinx.coroutines.test.runTest import okhttp3.OkHttpClient @@ -25,13 +25,13 @@ class OPMLFileTest { private lateinit var account: Account private lateinit var database: Database private lateinit var feedFixture: FeedFixture - private lateinit var tagFixture: TagFixture + private lateinit var tagFixture: FolderFixture @Before fun setup() { database = InMemoryDatabaseProvider.build(accountID) feedFixture = FeedFixture(database) - tagFixture = TagFixture(database) + tagFixture = FolderFixture(database) val delegate = LocalAccountDelegate( database = database, diff --git a/capy/src/test/java/com/jocmp/capy/accounts/reader/ReaderAccountDelegateTest.kt b/capy/src/test/java/com/jocmp/capy/accounts/reader/ReaderAccountDelegateTest.kt index 681d1842..79622bb5 100644 --- a/capy/src/test/java/com/jocmp/capy/accounts/reader/ReaderAccountDelegateTest.kt +++ b/capy/src/test/java/com/jocmp/capy/accounts/reader/ReaderAccountDelegateTest.kt @@ -9,6 +9,7 @@ import com.jocmp.capy.accounts.Source import com.jocmp.capy.articles.UnreadSortOrder import com.jocmp.capy.db.Database import com.jocmp.capy.fixtures.FeedFixture +import com.jocmp.capy.fixtures.FolderFixture import com.jocmp.capy.logging.CapyLog import com.jocmp.capy.persistence.ArticleRecords import com.jocmp.readerclient.Category @@ -49,6 +50,7 @@ class ReaderAccountDelegateTest { private lateinit var database: Database private lateinit var googleReader: GoogleReader private lateinit var feedFixture: FeedFixture + private lateinit var folderFixture: FolderFixture private lateinit var delegate: AccountDelegate private val arsTechnica = Subscription( @@ -93,11 +95,45 @@ class ReaderAccountDelegateTest { title = "Ars Technica - All content", htmlUrl = "https://arstechnica.com", ), + categories = listOf( + "user/-/label/Tech" + ), summary = Summary("Summary - Welcome to Edition 7.07 of the Rocket Report! SpaceX has not missed a beat since the Federal Aviation Administration gave the company a green light to resume Falcon 9 launches after a failure last month."), content = Item.Content("Content - Welcome to Edition 7.07 of the Rocket Report! SpaceX has not missed a beat since the Federal Aviation Administration gave the company a green light to resume Falcon 9 launches after a failure last month."), ) ) + private val unreadItem = Item( + id = "tag:google.com,2005:reader/item/0000000000000001", + published = 1708710158, + title = "Reddit admits more moderator protests could hurt its business", + canonical = listOf(Link("https://arstechnica.com/?p=2005526")), + author = "Scharon Harding", + origin = Origin( + streamId = "feed/2", + title = "Ars Technica - All content", + htmlUrl = "https://arstechnica.com", + ), + summary = Summary("Enlarge (credit: Jakub Porzycki/NurPhoto via Getty Images) Reddit filed to go public on Thursday (PDF), revealing various details of the social media company's inner workings. Among the revelations, Reddit acknowledged the threat of future user protests"), + ) + + private val readItem = Item( + id = "tag:google.com,2005:reader/item/0000000000000002", + title = "Apple’s iPhone 16 launch event is set for September", + summary = Summary("Apple’s tagline: 'It’s Glowtime.'"), + canonical = listOf(Link("https://www.theverge.com/2024/8/26/24223957/apple-iphone-16-launch-event-date-glowtime")), + published = 1724521358, + author = "Jay Peters", + origin = Origin( + streamId = "feed/3", + title = "The Verge", + htmlUrl = "https://theverge.com", + ), + categories = listOf( + "user/-/state/com.google/read" + ) + ) + @BeforeTest fun setup() { mockkObject(CapyLog) @@ -105,6 +141,7 @@ class ReaderAccountDelegateTest { database = InMemoryDatabaseProvider.build(accountID) feedFixture = FeedFixture(database) + folderFixture = FolderFixture(database) googleReader = mockk() delegate = ReaderAccountDelegate(source = Source.FRESHRSS, database, googleReader) @@ -117,7 +154,7 @@ class ReaderAccountDelegateTest { stubSubscriptions() stubUnread(itemRefs) stubStarred() - stubReadingList(itemRefs) + stubStreamItemsIDs(itemRefs) delegate.refresh(ArticleFilter.default()) @@ -145,45 +182,129 @@ class ReaderAccountDelegateTest { } @Test - fun refresh_findsMissingArticles() = runTest { - val itemRefs = listOf("1", "16").map { ItemRef(it) } + fun refresh_unreadOnly() = runTest { + val itemRefs = listOf(ItemRef("16")) stubSubscriptions() stubUnread(itemRefs) - stubStarred(listOf("1", "2").map { ItemRef(it) }) + stubStreamItemsIDs(itemRefs, stream = Stream.ReadingList()) - val unreadItem = Item( - id = "tag:google.com,2005:reader/item/0000000000000001", - published = 1708710158, - title = "Reddit admits more moderator protests could hurt its business", - canonical = listOf(Link("https://arstechnica.com/?p=2005526")), - author = "Scharon Harding", - origin = Origin( - streamId = "feed/2", - title = "Ars Technica - All content", - htmlUrl = "https://arstechnica.com", - ), - summary = Summary("Enlarge (credit: Jakub Porzycki/NurPhoto via Getty Images) Reddit filed to go public on Thursday (PDF), revealing various details of the social media company's inner workings. Among the revelations, Reddit acknowledged the threat of future user protests"), + delegate.refresh( + ArticleFilter.Articles(articleStatus = ArticleStatus.UNREAD) ) - val readItem = Item( - id = "tag:google.com,2005:reader/item/0000000000000002", - title = "Apple’s iPhone 16 launch event is set for September", - summary = Summary("Apple’s tagline: 'It’s Glowtime.'"), - canonical = listOf(Link("https://www.theverge.com/2024/8/26/24223957/apple-iphone-16-launch-event-date-glowtime")), - published = 1724521358, - author = "Jay Peters", - origin = Origin( - streamId = "feed/3", - title = "The Verge", - htmlUrl = "https://theverge.com", - ), - categories = listOf( - "user/-/state/com.google/read" + val articles = database + .articlesQueries + .countAll(read = false, starred = false) + .executeAsList() + + assertEquals(expected = 1, actual = articles.size) + } + + @Test + fun refresh_starredOnly() = runTest { + val itemRefs = listOf(ItemRef("16")) + + stubSubscriptions() + stubStarred() + stubStreamItemsIDs(itemRefs, stream = Stream.Starred()) + + delegate.refresh( + ArticleFilter.Articles(articleStatus = ArticleStatus.STARRED) + ) + + val articles = database + .articlesQueries + .countAll(read = false, starred = false) + .executeAsList() + + assertEquals(expected = 1, actual = articles.size) + } + + @Test + fun refresh_feedOnly() = runTest { + val id = "feed/2" + val itemRefs = listOf(ItemRef("16")) + + stubStreamItemsIDs(itemRefs, stream = Stream.Feed(id)) + + delegate.refresh( + ArticleFilter.Feeds( + feedID = id, + feedStatus = ArticleStatus.UNREAD, + folderTitle = "" ) ) - stubReadingList(itemRefs, listOf(unreadItem, items.first())) + val articles = database + .articlesQueries + .countAll(read = false, starred = false) + .executeAsList() + + assertEquals(expected = 1, actual = articles.size) + } + + @Test + fun refresh_folderOnly() = runTest { + val folderTitle = "Tech" + val feed = feedFixture.create(feedID = "feed/2") + folderFixture.create(name = folderTitle, feed = feed) + + val itemRefs = listOf(ItemRef("16")) + + stubStreamItemsIDs(itemRefs, stream = Stream.Label(folderTitle)) + + delegate.refresh( + ArticleFilter.Folders( + folderTitle = folderTitle, + folderStatus = ArticleStatus.UNREAD, + ) + ) + + val articles = database + .articlesQueries + .countAll(read = false, starred = false) + .executeAsList() + + assertEquals(expected = 1, actual = articles.size) + } + + @Test + fun `refresh Miniflux folder`() = runTest { + delegate = ReaderAccountDelegate(source = Source.READER, database, googleReader) + + val folderTitle = "Tech" + val feed = feedFixture.create(feedID = "feed/2") + folderFixture.create(name = folderTitle, feed = feed) + + val itemRefs = listOf(ItemRef("16")) + + stubStreamItemsIDs(itemRefs, stream = Stream.ReadingList()) + + delegate.refresh( + ArticleFilter.Folders( + folderTitle = folderTitle, + folderStatus = ArticleStatus.UNREAD, + ) + ) + + val articles = database + .articlesQueries + .countAll(read = false, starred = false) + .executeAsList() + + assertEquals(expected = 1, actual = articles.size) + } + + @Test + fun refresh_findsMissingArticles() = runTest { + val itemRefs = listOf("1", "16").map { ItemRef(it) } + + stubSubscriptions() + stubUnread(itemRefs) + stubStarred(listOf("1", "2").map { ItemRef(it) }) + + stubStreamItemsIDs(itemRefs, listOf(unreadItem, items.first())) val starredItems = listOf(unreadItem, readItem) @@ -228,7 +349,7 @@ class ReaderAccountDelegateTest { googleReader.editTag( ids = listOf(id), postToken = postToken, - addTag = Stream.READ.id, + addTag = Stream.Read().id, ) } returns Response.success("OK") @@ -240,7 +361,7 @@ class ReaderAccountDelegateTest { googleReader.editTag( ids = listOf(id), postToken = postToken, - addTag = Stream.READ.id, + addTag = Stream.Read().id, ) } } @@ -253,7 +374,7 @@ class ReaderAccountDelegateTest { googleReader.editTag( ids = listOf(id), postToken = postToken, - removeTag = Stream.READ.id, + removeTag = Stream.Read().id, ) } returns Response.success("OK") @@ -265,7 +386,7 @@ class ReaderAccountDelegateTest { googleReader.editTag( ids = listOf(id), postToken = postToken, - removeTag = Stream.READ.id, + removeTag = Stream.Read().id, ) } } @@ -278,7 +399,7 @@ class ReaderAccountDelegateTest { googleReader.editTag( ids = listOf(id), postToken = postToken, - addTag = Stream.STARRED.id, + addTag = Stream.Starred().id, ) } returns Response.success("OK") @@ -290,7 +411,7 @@ class ReaderAccountDelegateTest { googleReader.editTag( ids = listOf(id), postToken = postToken, - addTag = Stream.STARRED.id, + addTag = Stream.Starred().id, ) } } @@ -303,7 +424,7 @@ class ReaderAccountDelegateTest { googleReader.editTag( ids = listOf(id), postToken = postToken, - removeTag = Stream.STARRED.id, + removeTag = Stream.Starred().id, ) } returns Response.success("OK") @@ -315,7 +436,7 @@ class ReaderAccountDelegateTest { googleReader.editTag( ids = listOf(id), postToken = postToken, - removeTag = Stream.STARRED.id, + removeTag = Stream.Starred().id, ) } } @@ -325,7 +446,7 @@ class ReaderAccountDelegateTest { stubPostToken() stubUnread() stubStarred() - stubReadingList() + stubStreamItemsIDs() val subscription = Subscription( id = "feed/4", @@ -500,7 +621,7 @@ class ReaderAccountDelegateTest { private fun stubStarred(itemRefs: List = emptyList()) { coEvery { googleReader.streamItemsIDs( - streamID = Stream.STARRED.id, + streamID = Stream.Starred().id, ) }.returns( Response.success( @@ -512,13 +633,14 @@ class ReaderAccountDelegateTest { ) } - private fun stubReadingList( + private fun stubStreamItemsIDs( itemRefs: List = emptyList(), - items: List = this.items + items: List = this.items, + stream: Stream = Stream.ReadingList(), ) { coEvery { googleReader.streamItemsIDs( - streamID = Stream.READING_LIST.id, + streamID = stream.id, since = any(), count = 100, ) @@ -553,9 +675,9 @@ class ReaderAccountDelegateTest { private fun stubUnread(itemRefs: List = emptyList()) { coEvery { googleReader.streamItemsIDs( - streamID = Stream.READING_LIST.id, + streamID = Stream.ReadingList().id, count = 10_000, - excludedStreamID = Stream.READ.id, + excludedStreamID = Stream.Read().id, ) }.returns(Response.success(StreamItemIDsResult(itemRefs = itemRefs, continuation = null))) } diff --git a/capy/src/test/java/com/jocmp/capy/fixtures/TagFixture.kt b/capy/src/test/java/com/jocmp/capy/fixtures/FolderFixture.kt similarity index 91% rename from capy/src/test/java/com/jocmp/capy/fixtures/TagFixture.kt rename to capy/src/test/java/com/jocmp/capy/fixtures/FolderFixture.kt index 080d9065..61b1620e 100644 --- a/capy/src/test/java/com/jocmp/capy/fixtures/TagFixture.kt +++ b/capy/src/test/java/com/jocmp/capy/fixtures/FolderFixture.kt @@ -5,7 +5,7 @@ import com.jocmp.capy.RandomUUID import com.jocmp.capy.db.Database import com.jocmp.capy.persistence.TaggingRecords -class TagFixture(private val database: Database) { +class FolderFixture(private val database: Database) { private val feedFixture = FeedFixture(database) fun create( diff --git a/readerclient/src/main/java/com/jocmp/readerclient/Item.kt b/readerclient/src/main/java/com/jocmp/readerclient/Item.kt index e3f0964a..da0fe384 100644 --- a/readerclient/src/main/java/com/jocmp/readerclient/Item.kt +++ b/readerclient/src/main/java/com/jocmp/readerclient/Item.kt @@ -20,6 +20,9 @@ data class Item( val read: Boolean get() = isRead(categories) + val starred: Boolean + get() = isStarred(categories) + @JsonClass(generateAdapter = true) data class Origin( val streamId: String, @@ -52,8 +55,12 @@ data class Item( get() = enclosure?.find { it.type.startsWith("image") } } +/** open for testing */ internal fun isRead(categories: List?): Boolean { - val pattern = Regex("user/.*/state/com.google/read$") + return categories?.any { it.endsWith("state/com.google/read") } ?: false +} - return categories?.any { pattern.find(it) != null } ?: false +/** open for testing */ +internal fun isStarred(categories: List?): Boolean { + return categories?.any { it.endsWith("state/com.google/starred") } ?: false } diff --git a/readerclient/src/main/java/com/jocmp/readerclient/ItemRef.kt b/readerclient/src/main/java/com/jocmp/readerclient/ItemRef.kt index 728d91d9..7e3de629 100644 --- a/readerclient/src/main/java/com/jocmp/readerclient/ItemRef.kt +++ b/readerclient/src/main/java/com/jocmp/readerclient/ItemRef.kt @@ -6,5 +6,7 @@ import com.squareup.moshi.JsonClass data class ItemRef( val id: String, ) { - val hexID = String.format("%016x", id.toLong()) + val hexID = buildHexID(id) } + +fun buildHexID(id: String) = String.format("%016x", id.toLong()) diff --git a/readerclient/src/main/java/com/jocmp/readerclient/Stream.kt b/readerclient/src/main/java/com/jocmp/readerclient/Stream.kt index f530656f..9d8597fa 100644 --- a/readerclient/src/main/java/com/jocmp/readerclient/Stream.kt +++ b/readerclient/src/main/java/com/jocmp/readerclient/Stream.kt @@ -1,9 +1,16 @@ package com.jocmp.readerclient -enum class Stream(val id: String) { - READING_LIST("user/-/state/com.google/reading-list"), +sealed class Stream(val id: String) { + class ReadingList: Stream("user/-/state/com.google/reading-list") - STARRED("user/-/state/com.google/starred"), + class Starred: Stream("user/-/state/com.google/starred") - READ("user/-/state/com.google/read") + class Read: Stream("user/-/state/com.google/read") + + class Feed(id: String): Stream("user/-/$id") + + class Label(name: String): Stream("user/-/label/$name") + + val isStateStream: Boolean + get() = !(this is Feed || this is Label) }