diff --git a/app/src/main/java/com/jocmp/basilreader/ui/accounts/AccountViewModel.kt b/app/src/main/java/com/jocmp/basilreader/ui/accounts/AccountViewModel.kt index 9bf0a30a..c9d97db5 100644 --- a/app/src/main/java/com/jocmp/basilreader/ui/accounts/AccountViewModel.kt +++ b/app/src/main/java/com/jocmp/basilreader/ui/accounts/AccountViewModel.kt @@ -13,8 +13,9 @@ import com.jocmp.basil.AccountManager import com.jocmp.basil.Article import com.jocmp.basil.Feed import com.jocmp.basil.FeedFormEntry +import com.jocmp.basil.Filter import com.jocmp.basil.Folder -import com.jocmp.basil.feedPagingSource +import com.jocmp.basil.buildPager import com.jocmp.basilreader.selectAccount import com.jocmp.basilreader.selectedAccount import kotlinx.coroutines.flow.first @@ -29,18 +30,22 @@ class AccountViewModel( policy = neverEqualPolicy() ) + private val pager: MutableState?> = mutableStateOf(null) + init { viewModelScope.launch { val accountID = settings.data.first().selectedAccount() if (accountID != null) { - updateState(accountID) + selectAccount(accountID) } else { val firstID = accountManager.accountIDs().firstOrNull() firstID?.let { - selectAccount(firstID) + selectSettingsAccount(firstID) } } + + pager.value = account?.buildPager() } } @@ -54,8 +59,6 @@ class AccountViewModel( private val articleState = mutableStateOf(null) - private val pager = mutableStateOf?>(null) - val feeds: List get() = account?.feeds?.toList() ?: emptyList() @@ -65,9 +68,16 @@ class AccountViewModel( fun articles(): Pager? = pager.value fun selectFeed(feedID: String, onComplete: () -> Unit) { - this.feedID.value = feedID + val feed = account?.findFeed(feedID) + + this.feedID.value = feed?.id + + if (feed != null) { + pager.value = account?.buildPager( + filter = Filter.Feeds(feed = feed, status = Filter.Status.ALL), + ) + } - pager.value = account?.feedPagingSource(this.feedID.value) clearArticle() onComplete() @@ -80,20 +90,20 @@ class AccountViewModel( } fun selectArticle(articleID: String) { - articleState.value = account?.findArticle(articleID.toLong(), feedID.value?.toLongOrNull()) + articleState.value = account?.findArticle(articleID.toLong()) } fun clearArticle() { articleState.value = null } - private fun selectAccount(accountID: String) { + private fun selectSettingsAccount(accountID: String) { viewModelScope.launch { settings.selectAccount(accountID) } } - private fun updateState(accountID: String) { + private fun selectAccount(accountID: String) { accountState.value = accountManager.findByID(accountID) } diff --git a/app/src/main/java/com/jocmp/basilreader/ui/articles/ArticleList.kt b/app/src/main/java/com/jocmp/basilreader/ui/articles/ArticleList.kt index d8b888ff..a6aa67df 100644 --- a/app/src/main/java/com/jocmp/basilreader/ui/articles/ArticleList.kt +++ b/app/src/main/java/com/jocmp/basilreader/ui/articles/ArticleList.kt @@ -23,6 +23,7 @@ import androidx.paging.Pager import androidx.paging.compose.collectAsLazyPagingItems import com.jocmp.basil.Article import kotlinx.coroutines.launch +import java.time.format.DateTimeFormatter @OptIn(ExperimentalMaterialApi::class) @Composable @@ -61,7 +62,10 @@ fun ArticleList( } } ) { - Text(item?.title ?: "No title", fontSize = 20.sp) + item?.let { article -> + Text(article.title, fontSize = 20.sp) + Text(article.arrivedAt.format(DateTimeFormatter.BASIC_ISO_DATE)) + } } } } diff --git a/basil/src/main/java/com/jocmp/basil/Account.kt b/basil/src/main/java/com/jocmp/basil/Account.kt index 3b23bb2d..ae8e1c70 100644 --- a/basil/src/main/java/com/jocmp/basil/Account.kt +++ b/basil/src/main/java/com/jocmp/basil/Account.kt @@ -2,13 +2,15 @@ package com.jocmp.basil import com.jocmp.basil.accounts.AccountDelegate import com.jocmp.basil.accounts.LocalAccountDelegate -import com.jocmp.basil.articles.articleMapper -import com.jocmp.basil.db.Articles +import com.jocmp.basil.accounts.ParsedItem +import com.jocmp.basil.accounts.asOPML import com.jocmp.basil.db.Database -import com.jocmp.basil.feeds.FeedRecords import com.jocmp.basil.opml.Outline import com.jocmp.basil.opml.asFeed import com.jocmp.basil.opml.asFolder +import com.jocmp.basil.persistence.FeedRecords +import com.jocmp.basil.persistence.articleMapper +import com.jocmp.basil.shared.nowUTC import com.jocmp.feedfinder.FeedFinder import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.coroutineScope @@ -52,6 +54,10 @@ data class Account( addAll(folders.flatMap { it.feeds }) } + fun findFeed(feedID: String): Feed? { + return flattenedFeeds.find { it.id == feedID } + } + suspend fun addFolder(title: String): Folder { val folder = Folder(title = title) @@ -87,18 +93,7 @@ data class Account( launch { val items = delegate.fetchAll(feed) - items.forEach { item -> - database.articlesQueries.create( - feed_id = feed.primaryKey, - external_id = item.externalID, - title = item.title, - content_html = item.contentHTML, - url = item.url, - summary = item.summary, - image_url = item.imageURL, - published_at = item.publishedAt?.toEpochSecond() - ) - } + updateArticles(feed, items) } } @@ -129,7 +124,22 @@ data class Account( val items = delegate.fetchAll(feed) + updateArticles(feed, items) + } + + fun findArticle(articleID: Long?): Article? { + articleID ?: return null + + return database.articlesQueries.findBy( + articleID = articleID, + mapper = ::articleMapper + ).executeAsOneOrNull() + } + + private fun updateArticles(feed: Feed, items: List) { items.forEach { item -> + val publishedAt = item.publishedAt?.toEpochSecond() + database.articlesQueries.create( feed_id = feed.primaryKey, external_id = item.externalID, @@ -138,21 +148,12 @@ data class Account( url = item.url, summary = item.summary, image_url = item.imageURL, - published_at = item.publishedAt?.toEpochSecond() + published_at = publishedAt, + arrived_at = publishedAt ?: nowUTC() ) } } - fun findArticle(articleID: Long?, feedID: Long?): Article? { - articleID ?: return null - - return database.articlesQueries.findBy( - articleID = articleID, - feedID = feedID, - mapper = ::articleMapper - ).executeAsOneOrNull() - } - private fun entrySiteURL(url: URL?): String { return url?.toString() ?: "" } diff --git a/basil/src/main/java/com/jocmp/basil/AccountPager.kt b/basil/src/main/java/com/jocmp/basil/AccountPager.kt new file mode 100644 index 00000000..e06221f2 --- /dev/null +++ b/basil/src/main/java/com/jocmp/basil/AccountPager.kt @@ -0,0 +1,19 @@ +package com.jocmp.basil + +import androidx.paging.Pager +import androidx.paging.PagingConfig +import com.jocmp.basil.persistence.ArticlePagerFactory + +fun Account.buildPager( + filter: Filter = Filter.Articles(status = Filter.Status.ALL) +): Pager { + val pagerFactory = ArticlePagerFactory( + database = database, + filter = filter + ) + + return Pager( + config = PagingConfig(pageSize = 10), + pagingSourceFactory = { pagerFactory.find() } + ) +} diff --git a/basil/src/main/java/com/jocmp/basil/Article.kt b/basil/src/main/java/com/jocmp/basil/Article.kt index 82582566..0e7b87f0 100644 --- a/basil/src/main/java/com/jocmp/basil/Article.kt +++ b/basil/src/main/java/com/jocmp/basil/Article.kt @@ -1,6 +1,7 @@ package com.jocmp.basil import java.net.URL +import java.time.ZonedDateTime data class Article( val id: String, @@ -10,5 +11,6 @@ data class Article( val contentHTML: String, val url: URL?, val summary: String, - val imageURL: URL? + val imageURL: URL?, + val arrivedAt: ZonedDateTime ) diff --git a/basil/src/main/java/com/jocmp/basil/ArticlesPagerExt.kt b/basil/src/main/java/com/jocmp/basil/ArticlesPagerExt.kt deleted file mode 100644 index b40581b3..00000000 --- a/basil/src/main/java/com/jocmp/basil/ArticlesPagerExt.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.jocmp.basil - -import androidx.paging.Pager -import androidx.paging.PagingConfig -import app.cash.sqldelight.paging3.QueryPagingSource -import com.jocmp.basil.articles.articleMapper -import kotlinx.coroutines.Dispatchers - -fun Account.feedPagingSource(feedID: String?): Pager { - return Pager( - config = PagingConfig(pageSize = 10), - pagingSourceFactory = { - QueryPagingSource( - countQuery = database.articlesQueries.countByFeed(feedID?.toLongOrNull()), - transacter = database.articlesQueries, - context = Dispatchers.IO, - queryProvider = { limit, offset -> - database.articlesQueries.allByFeed( - feedID = feedID?.toLongOrNull(), - limit = limit, - offset = offset, - mapper = ::articleMapper - ) - } - ) - } - ) -} diff --git a/basil/src/main/java/com/jocmp/basil/Feed.kt b/basil/src/main/java/com/jocmp/basil/Feed.kt index e72127de..ea64096d 100644 --- a/basil/src/main/java/com/jocmp/basil/Feed.kt +++ b/basil/src/main/java/com/jocmp/basil/Feed.kt @@ -23,9 +23,3 @@ data class Feed( return id.hashCode() } } - -fun Feed.asOPML(indentLevel: Int): String { - val opml = - "\n" - return opml.prepending(tabCount = indentLevel) -} diff --git a/basil/src/main/java/com/jocmp/basil/Filter.kt b/basil/src/main/java/com/jocmp/basil/Filter.kt new file mode 100644 index 00000000..4b61d12c --- /dev/null +++ b/basil/src/main/java/com/jocmp/basil/Filter.kt @@ -0,0 +1,15 @@ +package com.jocmp.basil + +sealed class Filter(val status: Status) { + enum class Status(value: String) { + ALL("all"), + READ("read"), + STARRED("starred") + } + + class Articles(status: Status) : Filter(status) + + class Feeds(val feed: Feed, status: Status) : Filter(status) + + class Folders(val folder: Folder, status: Status) : Filter(status) +} diff --git a/basil/src/main/java/com/jocmp/basil/Folder.kt b/basil/src/main/java/com/jocmp/basil/Folder.kt index 18a48eec..ff8e213d 100644 --- a/basil/src/main/java/com/jocmp/basil/Folder.kt +++ b/basil/src/main/java/com/jocmp/basil/Folder.kt @@ -19,21 +19,3 @@ data class Folder( return 31 * result } } - -fun Folder.asOPML(indentLevel: Int = 0): String { - if (feeds.isEmpty()) { - val opml = "\n" - return opml.prepending(tabCount = indentLevel) - } - - var opml = "\n" - opml = opml.prepending(tabCount = indentLevel) - - feeds.forEach { feed -> - opml += feed.asOPML(indentLevel = indentLevel + 1) - } - - opml = opml + repeatTab(tabCount = indentLevel) + "\n" - - return opml -} diff --git a/basil/src/main/java/com/jocmp/basil/accounts/AccountDelegate.kt b/basil/src/main/java/com/jocmp/basil/accounts/AccountDelegate.kt index 0b65410d..6d648478 100644 --- a/basil/src/main/java/com/jocmp/basil/accounts/AccountDelegate.kt +++ b/basil/src/main/java/com/jocmp/basil/accounts/AccountDelegate.kt @@ -1,7 +1,6 @@ package com.jocmp.basil.accounts import com.jocmp.basil.Feed -import com.jocmp.basil.feeds.ExternalFeed import java.net.URL internal interface AccountDelegate { diff --git a/basil/src/main/java/com/jocmp/basil/feeds/ExternalFeed.kt b/basil/src/main/java/com/jocmp/basil/accounts/ExternalFeed.kt similarity index 72% rename from basil/src/main/java/com/jocmp/basil/feeds/ExternalFeed.kt rename to basil/src/main/java/com/jocmp/basil/accounts/ExternalFeed.kt index 268f1747..e57b4478 100644 --- a/basil/src/main/java/com/jocmp/basil/feeds/ExternalFeed.kt +++ b/basil/src/main/java/com/jocmp/basil/accounts/ExternalFeed.kt @@ -1,4 +1,4 @@ -package com.jocmp.basil.feeds +package com.jocmp.basil.accounts internal data class ExternalFeed( val externalID: String, diff --git a/basil/src/main/java/com/jocmp/basil/accounts/FeedOPMLExt.kt b/basil/src/main/java/com/jocmp/basil/accounts/FeedOPMLExt.kt new file mode 100644 index 00000000..d6b77d2d --- /dev/null +++ b/basil/src/main/java/com/jocmp/basil/accounts/FeedOPMLExt.kt @@ -0,0 +1,10 @@ +package com.jocmp.basil.accounts + +import com.jocmp.basil.Feed +import com.jocmp.basil.shared.prepending + +fun Feed.asOPML(indentLevel: Int): String { + val opml = + "\n" + return opml.prepending(tabCount = indentLevel) +} diff --git a/basil/src/main/java/com/jocmp/basil/accounts/FolderOPMLExt.kt b/basil/src/main/java/com/jocmp/basil/accounts/FolderOPMLExt.kt new file mode 100644 index 00000000..69b1254f --- /dev/null +++ b/basil/src/main/java/com/jocmp/basil/accounts/FolderOPMLExt.kt @@ -0,0 +1,23 @@ +package com.jocmp.basil.accounts + +import com.jocmp.basil.Folder +import com.jocmp.basil.shared.prepending +import com.jocmp.basil.shared.repeatTab + +internal fun Folder.asOPML(indentLevel: Int = 0): String { + if (feeds.isEmpty()) { + val opml = "\n" + return opml.prepending(tabCount = indentLevel) + } + + var opml = "\n" + opml = opml.prepending(tabCount = indentLevel) + + feeds.forEach { feed -> + opml += feed.asOPML(indentLevel = indentLevel + 1) + } + + opml = opml + repeatTab(tabCount = indentLevel) + "\n" + + return opml +} diff --git a/basil/src/main/java/com/jocmp/basil/accounts/LocalAccountDelegate.kt b/basil/src/main/java/com/jocmp/basil/accounts/LocalAccountDelegate.kt index d0b67a60..70d90d96 100644 --- a/basil/src/main/java/com/jocmp/basil/accounts/LocalAccountDelegate.kt +++ b/basil/src/main/java/com/jocmp/basil/accounts/LocalAccountDelegate.kt @@ -2,12 +2,10 @@ package com.jocmp.basil.accounts import com.jocmp.basil.Account import com.jocmp.basil.Feed -import com.jocmp.basil.feeds.ExternalFeed import com.jocmp.basil.shared.parseISODate import com.prof18.rssparser.RssParser import com.prof18.rssparser.model.RssItem import java.net.URL -import java.time.ZonedDateTime internal class LocalAccountDelegate(private val account: Account) : AccountDelegate { override suspend fun createFeed(feedURL: URL): ExternalFeed { diff --git a/basil/src/main/java/com/jocmp/basil/articles/ArticleMapper.kt b/basil/src/main/java/com/jocmp/basil/persistence/ArticleMapper.kt similarity index 53% rename from basil/src/main/java/com/jocmp/basil/articles/ArticleMapper.kt rename to basil/src/main/java/com/jocmp/basil/persistence/ArticleMapper.kt index 9742915d..e595d624 100644 --- a/basil/src/main/java/com/jocmp/basil/articles/ArticleMapper.kt +++ b/basil/src/main/java/com/jocmp/basil/persistence/ArticleMapper.kt @@ -1,27 +1,35 @@ -package com.jocmp.basil.articles +package com.jocmp.basil.persistence import com.jocmp.basil.Article import com.jocmp.basil.shared.optionalURL +import java.time.Instant +import java.time.ZoneId +import java.time.ZonedDateTime internal fun articleMapper( id: Long, externalID: String, - mapperFeedID: Long?, + feedID: Long?, title: String?, contentHtml: String?, url: String?, summary: String?, imageURL: String?, - publishedAt: Long? + publishedAt: Long?, + arrivedAt: Long?, + read: Boolean?, + starred: Boolean?, + zoneID: ZoneId = ZoneId.systemDefault() ): Article { return Article( id = id.toString(), externalID = externalID, - feedID = mapperFeedID.toString(), + feedID = feedID.toString(), title = title ?: "", contentHTML = contentHtml ?: "", url = optionalURL(url), imageURL = optionalURL(imageURL), - summary = summary ?: "" + summary = summary ?: "", + arrivedAt = ZonedDateTime.ofInstant(Instant.ofEpochSecond(arrivedAt!!), zoneID) ) } diff --git a/basil/src/main/java/com/jocmp/basil/persistence/ArticlePagerFactory.kt b/basil/src/main/java/com/jocmp/basil/persistence/ArticlePagerFactory.kt new file mode 100644 index 00000000..d299fa89 --- /dev/null +++ b/basil/src/main/java/com/jocmp/basil/persistence/ArticlePagerFactory.kt @@ -0,0 +1,78 @@ +package com.jocmp.basil.persistence + +import androidx.paging.PagingSource +import app.cash.sqldelight.paging3.QueryPagingSource +import com.jocmp.basil.Article +import com.jocmp.basil.Filter +import com.jocmp.basil.db.Database +import kotlinx.coroutines.Dispatchers + +internal class ArticlePagerFactory( + private val database: Database, + private val filter: Filter, +) { + fun find(): PagingSource { + return when (filter) { + is Filter.Articles -> articleSource(filter) + is Filter.Feeds -> feedSource(filter) + is Filter.Folders -> folderSource(filter) + } + } + + private fun articleSource(filter: Filter.Articles): PagingSource { + return QueryPagingSource( + countQuery = database.articlesQueries.countByStatus( + read = listOf(false), + starred = listOf(false), + ), + transacter = database.articlesQueries, + context = Dispatchers.IO, + queryProvider = { limit, offset -> + database.articlesQueries.findByStatus( + read = listOf(false), + starred = listOf(false), + limit = limit, + offset = offset, + mapper = ::articleMapper + ) + } + ) + } + + private fun feedSource(filter: Filter.Feeds): PagingSource { + val feedIDs = listOf(filter.feed.id.toLong()) + + return feedsSource(feedIDs, filter.status) + } + + private fun folderSource(filter: Filter.Folders): PagingSource { + val feedIDs = filter.folder.feeds.mapNotNull { it.id.toLongOrNull() } + + return feedsSource(feedIDs, filter.status) + } + + private fun feedsSource( + feedIDs: List, + status: Filter.Status + ): PagingSource { + return QueryPagingSource( + countQuery = database.articlesQueries.countByFeeds( + feedIDs = feedIDs, + read = listOf(false), + starred = listOf(false), + ), + transacter = database.articlesQueries, + context = Dispatchers.IO, + queryProvider = { limit, offset -> + database.articlesQueries.findByFeeds( + feedIDs = feedIDs, + read = listOf(false), + starred = listOf(false), + limit = limit, + offset = offset, + mapper = ::articleMapper + ) + } + ) + } +} diff --git a/basil/src/main/java/com/jocmp/basil/persistence/ArticleRecords.kt b/basil/src/main/java/com/jocmp/basil/persistence/ArticleRecords.kt new file mode 100644 index 00000000..f79146be --- /dev/null +++ b/basil/src/main/java/com/jocmp/basil/persistence/ArticleRecords.kt @@ -0,0 +1,8 @@ +package com.jocmp.basil.persistence + +import com.jocmp.basil.db.Database + +internal class ArticleRecords(database: Database) { + fun create() { + } +} diff --git a/basil/src/main/java/com/jocmp/basil/feeds/FeedRecords.kt b/basil/src/main/java/com/jocmp/basil/persistence/FeedRecords.kt similarity index 88% rename from basil/src/main/java/com/jocmp/basil/feeds/FeedRecords.kt rename to basil/src/main/java/com/jocmp/basil/persistence/FeedRecords.kt index 9099f2ad..a3040b63 100644 --- a/basil/src/main/java/com/jocmp/basil/feeds/FeedRecords.kt +++ b/basil/src/main/java/com/jocmp/basil/persistence/FeedRecords.kt @@ -1,7 +1,8 @@ -package com.jocmp.basil.feeds +package com.jocmp.basil.persistence import com.jocmp.basil.db.Database import com.jocmp.basil.db.Feeds +import com.jocmp.basil.accounts.ExternalFeed internal class FeedRecords(val database: Database) { internal fun findOrCreate(externalFeed: ExternalFeed): Feeds { diff --git a/basil/src/main/java/com/jocmp/basil/shared/TimeHelpers.kt b/basil/src/main/java/com/jocmp/basil/shared/TimeHelpers.kt index 5a397a3c..beacf06b 100644 --- a/basil/src/main/java/com/jocmp/basil/shared/TimeHelpers.kt +++ b/basil/src/main/java/com/jocmp/basil/shared/TimeHelpers.kt @@ -13,3 +13,7 @@ fun parseISODate(value: String?): OffsetDateTime? { null } } + +fun nowUTC(): Long { + return OffsetDateTime.now(ZoneOffset.UTC).toEpochSecond() +} diff --git a/basil/src/main/sqldelight/com/jocmp/basil/db/article_statuses.sq b/basil/src/main/sqldelight/com/jocmp/basil/db/article_statuses.sq new file mode 100644 index 00000000..469b5d79 --- /dev/null +++ b/basil/src/main/sqldelight/com/jocmp/basil/db/article_statuses.sq @@ -0,0 +1,9 @@ +import kotlin.Boolean; + +CREATE TABLE article_statuses ( + id INTEGER NOT NULL PRIMARY KEY, + article_id INTEGER NOT NULL REFERENCES articles(id), + read INTEGER AS Boolean DEFAULT 0, + starred INTEGER AS Boolean DEFAULT 0, + arrived_at INTEGER NOT NULL +); diff --git a/basil/src/main/sqldelight/com/jocmp/basil/db/articles.sq b/basil/src/main/sqldelight/com/jocmp/basil/db/articles.sq index c811e357..97b857a2 100644 --- a/basil/src/main/sqldelight/com/jocmp/basil/db/articles.sq +++ b/basil/src/main/sqldelight/com/jocmp/basil/db/articles.sq @@ -1,5 +1,5 @@ -CREATE TABLE articles( - id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, +CREATE TABLE IF NOT EXISTS articles( + id INTEGER NOT NULL PRIMARY KEY, external_id TEXT NOT NULL UNIQUE, feed_id INTEGER REFERENCES feeds(id), title TEXT, @@ -10,42 +10,83 @@ CREATE TABLE articles( published_at INTEGER ); -countByFeed: +CREATE UNIQUE INDEX articles_external_id_index ON articles(external_id); + +countByStatus: SELECT COUNT(*) FROM articles -WHERE feed_id = :feedID; +JOIN article_statuses ON articles.id = article_statuses.article_id +WHERE article_statuses.read IN :read +AND article_statuses.starred IN :starred; -allByFeed: -SELECT * +findByStatus: +SELECT + articles.*, + article_statuses.arrived_at, + article_statuses.starred, + article_statuses.read FROM articles -WHERE feed_id = :feedID -ORDER BY published_at DESC +JOIN article_statuses ON articles.id = article_statuses.article_id +AND article_statuses.read IN :read +AND article_statuses.starred IN :starred +ORDER BY article_statuses.arrived_at DESC +LIMIT :limit OFFSET :offset; + +countByFeeds: +SELECT COUNT(*) +FROM articles +JOIN article_statuses ON articles.id = article_statuses.article_id +WHERE feed_id IN :feedIDs +AND article_statuses.read IN :read +AND article_statuses.starred IN :starred; + +findByFeeds: +SELECT + articles.*, + article_statuses.arrived_at, + article_statuses.starred, + article_statuses.read +FROM articles +JOIN article_statuses ON articles.id = article_statuses.article_id +WHERE feed_id IN :feedIDs +AND article_statuses.read IN :read +AND article_statuses.starred IN :starred +ORDER BY article_statuses.arrived_at DESC LIMIT :limit OFFSET :offset; findBy: -SELECT * +SELECT + articles.*, + article_statuses.arrived_at, + article_statuses.starred, + article_statuses.read FROM articles -WHERE feed_id = :feedID AND id = :articleID +JOIN article_statuses ON articles.id = article_statuses.article_id +WHERE articles.id = :articleID LIMIT 1; -create: -REPLACE INTO articles( - feed_id, - external_id, - title, - content_html, - url, - summary, - image_url, - published_at -) -VALUES ( -?, -?, -?, -?, -?, -?, -?, -? -); +create { + REPLACE INTO articles( + feed_id, + external_id, + title, + content_html, + url, + summary, + image_url, + published_at + ) + VALUES ( + ?, + ?, + ?, + ?, + ?, + ?, + ?, + ? + ); + + INSERT OR IGNORE INTO article_statuses(article_id, arrived_at) + VALUES (last_insert_rowid(), ?); +} diff --git a/basil/src/main/sqldelight/com/jocmp/basil/db/feeds.sq b/basil/src/main/sqldelight/com/jocmp/basil/db/feeds.sq index 77554e98..b19a53d0 100644 --- a/basil/src/main/sqldelight/com/jocmp/basil/db/feeds.sq +++ b/basil/src/main/sqldelight/com/jocmp/basil/db/feeds.sq @@ -1,5 +1,5 @@ CREATE TABLE feeds ( - id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + id INTEGER NOT NULL PRIMARY KEY, external_id TEXT NOT NULL, feed_url TEXT NOT NULL UNIQUE ); diff --git a/basil/src/test/java/com/jocmp/basil/persistence/ArticleMapperTest.kt b/basil/src/test/java/com/jocmp/basil/persistence/ArticleMapperTest.kt new file mode 100644 index 00000000..fd625e24 --- /dev/null +++ b/basil/src/test/java/com/jocmp/basil/persistence/ArticleMapperTest.kt @@ -0,0 +1,42 @@ +package com.jocmp.basil.persistence + +import org.junit.Test +import java.time.ZoneId +import java.time.ZonedDateTime +import kotlin.test.assertEquals + +class ArticleMapperTest { + @Test + fun `it maps to the system's timezone`() { + val zoneID = ZoneId.of("America/Chicago") + + val article = articleMapper( + id = 1, + externalID = "https://www.theverge.com/2023/12/30/24019780/vizio-settlement-effective-refresh-rate-class-action-lawsuit", + feedID = 1, + title = "Vizio agrees to pay $3 million for alleged ‘false’ refresh rate claims", + contentHtml = "

If you bought a Vizio TV in California after April 30th, 2014, Vizio may owe you some money

", + url = "https://www.theverge.com/2023/12/30/24019780/vizio-settlement-effective-refresh-rate-class-action-lawsuit", + summary = "", + imageURL = "https://cdn.vox-cdn.com/thumbor/r-eWiuX74LfGvTxwenExmwmkPlk=/0x0:1800x1200/1310x873/cdn.vox-cdn.com/uploads/chorus_image/image/73010063/Vizio_TV_D_Series_Lifestyle.0.jpg", + publishedAt = 1703960809, + arrivedAt = 1703960809, + read = false, + starred = false, + zoneID = zoneID + ) + + val expectedTime = ZonedDateTime.of( + 2023, + 12, + 30, + 12, + 26, + 49, + 0, + zoneID + ) + + assertEquals(expected = expectedTime, actual = article.arrivedAt) + } +}