From 92b97eeccf93025b313796f35c306f9a12550bfc Mon Sep 17 00:00:00 2001 From: Sasikanth Miriyampalli Date: Sat, 13 Apr 2024 05:58:15 +0530 Subject: [PATCH 1/8] Add support for unique post ID column (#440) We are creating a UUID based on the post link --- .../rss/reader/core/model/local/Post.kt | 1 + .../core/model/local/PostWithMetadata.kt | 1 + .../rss/reader/database/DriverFactory.kt | 8 +- .../sasikanth/rss/reader/app/AppPresenter.kt | 13 +- .../rss/reader/bookmarks/BookmarksEvent.kt | 2 +- .../reader/bookmarks/BookmarksPresenter.kt | 23 ++- .../reader/bookmarks/ui/BookmarksScreen.kt | 2 +- .../database/migrations/SQLCodeMigrations.kt | 64 ++++++++ .../sasikanth/rss/reader/di/DataComponent.kt | 4 + .../sasikanth/rss/reader/home/HomeEvent.kt | 2 +- .../rss/reader/home/HomePresenter.kt | 16 +- .../rss/reader/home/ui/FeaturedSection.kt | 2 +- .../rss/reader/home/ui/HomeScreen.kt | 4 +- .../sasikanth/rss/reader/home/ui/PostList.kt | 2 +- .../rss/reader/reader/ReaderEvent.kt | 2 +- .../rss/reader/reader/ReaderPresenter.kt | 45 +++--- .../rss/reader/repository/RssRepository.kt | 22 ++- .../rss/reader/search/SearchEvent.kt | 2 +- .../rss/reader/search/SearchPresenter.kt | 16 +- .../rss/reader/search/ui/SearchScreen.kt | 2 +- .../src/commonMain/sqldelight/databases/13.db | Bin 0 -> 86016 bytes .../sasikanth/rss/reader/database/Bookmark.sq | 18 +-- .../dev/sasikanth/rss/reader/database/Post.sq | 33 +++-- .../rss/reader/database/PostSearchFTS.sq | 18 ++- .../commonMain/sqldelight/migrations/12.sqm | 140 ++++++++++++++++++ .../rss/reader/database/DriverFactory.kt | 12 +- 26 files changed, 337 insertions(+), 117 deletions(-) create mode 100644 shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/database/migrations/SQLCodeMigrations.kt create mode 100644 shared/src/commonMain/sqldelight/databases/13.db create mode 100644 shared/src/commonMain/sqldelight/migrations/12.sqm diff --git a/core/model/src/commonMain/kotlin/dev/sasikanth/rss/reader/core/model/local/Post.kt b/core/model/src/commonMain/kotlin/dev/sasikanth/rss/reader/core/model/local/Post.kt index 590a5bb8b..cd05a7dd0 100644 --- a/core/model/src/commonMain/kotlin/dev/sasikanth/rss/reader/core/model/local/Post.kt +++ b/core/model/src/commonMain/kotlin/dev/sasikanth/rss/reader/core/model/local/Post.kt @@ -19,6 +19,7 @@ package dev.sasikanth.rss.reader.core.model.local import kotlinx.datetime.Instant data class Post( + val id: String, val title: String, val description: String, val imageUrl: String?, diff --git a/core/model/src/commonMain/kotlin/dev/sasikanth/rss/reader/core/model/local/PostWithMetadata.kt b/core/model/src/commonMain/kotlin/dev/sasikanth/rss/reader/core/model/local/PostWithMetadata.kt index 70c5d9501..8a66b0af5 100644 --- a/core/model/src/commonMain/kotlin/dev/sasikanth/rss/reader/core/model/local/PostWithMetadata.kt +++ b/core/model/src/commonMain/kotlin/dev/sasikanth/rss/reader/core/model/local/PostWithMetadata.kt @@ -20,6 +20,7 @@ import kotlinx.datetime.Instant @Immutable data class PostWithMetadata( + val id: String, val title: String, val description: String, val imageUrl: String?, diff --git a/shared/src/androidMain/kotlin/dev/sasikanth/rss/reader/database/DriverFactory.kt b/shared/src/androidMain/kotlin/dev/sasikanth/rss/reader/database/DriverFactory.kt index 6c9bb4574..4950c22c8 100644 --- a/shared/src/androidMain/kotlin/dev/sasikanth/rss/reader/database/DriverFactory.kt +++ b/shared/src/androidMain/kotlin/dev/sasikanth/rss/reader/database/DriverFactory.kt @@ -17,6 +17,7 @@ package dev.sasikanth.rss.reader.database import android.content.Context import androidx.sqlite.db.SupportSQLiteDatabase +import app.cash.sqldelight.db.AfterVersion import app.cash.sqldelight.db.SqlDriver import app.cash.sqldelight.driver.android.AndroidSqliteDriver import dev.sasikanth.rss.reader.di.scopes.AppScope @@ -25,7 +26,10 @@ import me.tatarka.inject.annotations.Inject @Inject @AppScope -actual class DriverFactory(private val context: Context) { +actual class DriverFactory( + private val context: Context, + private val codeMigrations: Array, +) { actual fun createDriver(): SqlDriver { return AndroidSqliteDriver( @@ -33,7 +37,7 @@ actual class DriverFactory(private val context: Context) { context = context, name = DB_NAME, callback = - object : AndroidSqliteDriver.Callback(ReaderDatabase.Schema) { + object : AndroidSqliteDriver.Callback(ReaderDatabase.Schema, callbacks = codeMigrations) { override fun onOpen(db: SupportSQLiteDatabase) { db.setForeignKeyConstraintsEnabled(true) } diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/app/AppPresenter.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/app/AppPresenter.kt index 936d7d634..09b1f9386 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/app/AppPresenter.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/app/AppPresenter.kt @@ -33,6 +33,7 @@ import com.arkivanov.essenty.lifecycle.coroutines.coroutineScope import com.arkivanov.essenty.lifecycle.doOnStart import dev.sasikanth.rss.reader.about.AboutPresenterFactory import dev.sasikanth.rss.reader.bookmarks.BookmarksPresenterFactory +import dev.sasikanth.rss.reader.core.model.local.PostWithMetadata import dev.sasikanth.rss.reader.di.scopes.ActivityScope import dev.sasikanth.rss.reader.feed.FeedPresenterFactory import dev.sasikanth.rss.reader.home.HomePresenterFactory @@ -160,21 +161,21 @@ class AppPresenter( } is Config.Reader -> { Screen.Reader( - presenter = readerPresenter(config.postLink, componentContext) { navigation.pop() } + presenter = readerPresenter(config.postId, componentContext) { navigation.pop() } ) } } - private fun openPost(postLink: String) { + private fun openPost(post: PostWithMetadata) { scope.launch { val showReaderView = withContext(dispatchersProvider.io) { settingsRepository.showReaderView.first() } if (showReaderView) { - navigation.push(Config.Reader(postLink)) + navigation.push(Config.Reader(post.id)) } else { - linkHandler.openLink(postLink) - rssRepository.updatePostReadStatus(read = true, link = postLink) + linkHandler.openLink(post.link) + rssRepository.updatePostReadStatus(read = true, id = post.id) } } } @@ -214,7 +215,7 @@ class AppPresenter( @Serializable data object About : Config - @Serializable data class Reader(val postLink: String) : Config + @Serializable data class Reader(val postId: String) : Config } @Serializable diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/bookmarks/BookmarksEvent.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/bookmarks/BookmarksEvent.kt index dfec4f611..8bc56eb97 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/bookmarks/BookmarksEvent.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/bookmarks/BookmarksEvent.kt @@ -27,5 +27,5 @@ sealed interface BookmarksEvent { data class OnPostClicked(val post: PostWithMetadata) : BookmarksEvent - data class TogglePostReadStatus(val postLink: String, val postRead: Boolean) : BookmarksEvent + data class TogglePostReadStatus(val postId: String, val postRead: Boolean) : BookmarksEvent } diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/bookmarks/BookmarksPresenter.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/bookmarks/BookmarksPresenter.kt index 28e4a452b..ca82e508c 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/bookmarks/BookmarksPresenter.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/bookmarks/BookmarksPresenter.kt @@ -43,7 +43,7 @@ internal typealias BookmarksPresenterFactory = ( ComponentContext, goBack: () -> Unit, - openReaderView: (String) -> Unit, + openReaderView: (PostWithMetadata) -> Unit, ) -> BookmarksPresenter @Inject @@ -52,7 +52,7 @@ class BookmarksPresenter( private val rssRepository: RssRepository, @Assisted componentContext: ComponentContext, @Assisted private val goBack: () -> Unit, - @Assisted private val openReaderView: (postLink: String) -> Unit, + @Assisted private val openReaderView: (post: PostWithMetadata) -> Unit, ) : ComponentContext by componentContext { private val presenterInstance = @@ -110,28 +110,25 @@ class BookmarksPresenter( is BookmarksEvent.OnPostClicked -> { // no-op } - is BookmarksEvent.TogglePostReadStatus -> - togglePostReadStatus(event.postLink, event.postRead) + is BookmarksEvent.TogglePostReadStatus -> togglePostReadStatus(event.postId, event.postRead) } } - private fun togglePostReadStatus(postLink: String, postRead: Boolean) { - coroutineScope.launch { - rssRepository.updatePostReadStatus(read = !postRead, link = postLink) - } + private fun togglePostReadStatus(postId: String, postRead: Boolean) { + coroutineScope.launch { rssRepository.updatePostReadStatus(read = !postRead, id = postId) } } fun onPostClicked( post: PostWithMetadata, - openReaderView: (postLink: String) -> Unit, + openReaderView: (post: PostWithMetadata) -> Unit, openLink: (postLink: String) -> Unit ) { coroutineScope.launch { - val hasPost = rssRepository.hasPost(post.link) + val hasPost = rssRepository.hasPost(post.id) val hasFeed = rssRepository.hasFeed(post.feedLink) if (hasPost && hasFeed) { - openReaderView(post.link) + openReaderView(post) } else { openLink(post.link) } @@ -141,9 +138,9 @@ class BookmarksPresenter( private fun onPostBookmarkClicked(post: PostWithMetadata) { coroutineScope.launch { if (rssRepository.hasFeed(post.feedLink)) { - rssRepository.updateBookmarkStatus(bookmarked = !post.bookmarked, link = post.link) + rssRepository.updateBookmarkStatus(bookmarked = !post.bookmarked, id = post.id) } else { - rssRepository.deleteBookmark(link = post.link) + rssRepository.deleteBookmark(id = post.id) } } } diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/bookmarks/ui/BookmarksScreen.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/bookmarks/ui/BookmarksScreen.kt index 47ff38ebd..dd349cd81 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/bookmarks/ui/BookmarksScreen.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/bookmarks/ui/BookmarksScreen.kt @@ -144,7 +144,7 @@ internal fun BookmarksScreen( }, togglePostReadClick = { bookmarksPresenter.dispatch( - BookmarksEvent.TogglePostReadStatus(post.link, post.read) + BookmarksEvent.TogglePostReadStatus(post.id, post.read) ) } ) diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/database/migrations/SQLCodeMigrations.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/database/migrations/SQLCodeMigrations.kt new file mode 100644 index 000000000..0906a8f7c --- /dev/null +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/database/migrations/SQLCodeMigrations.kt @@ -0,0 +1,64 @@ +/* + * Copyright 2024 Sasikanth Miriyampalli + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.sasikanth.rss.reader.database.migrations + +import app.cash.sqldelight.Query +import app.cash.sqldelight.db.AfterVersion +import app.cash.sqldelight.db.QueryResult +import app.cash.sqldelight.db.SqlCursor +import app.cash.sqldelight.db.SqlDriver +import dev.sasikanth.rss.reader.utils.nameBasedUuidOf + +object SQLCodeMigrations { + + fun migrations(): Array { + return arrayOf(afterVersion12()) + } + + private fun afterVersion12(): AfterVersion { + return AfterVersion(12) { driver -> + val ids = PostsIdsQuery(driver).executeAsList() + ids.forEach { id -> + driver.execute( + identifier = null, + sql = "UPDATE post SET id = ? WHERE id = ?", + parameters = 2, + ) { + bindString(0, nameBasedUuidOf(id).toString()) + bindString(1, id) + } + } + } + } +} + +private class PostsIdsQuery( + private val driver: SqlDriver, +) : Query(mapper = { cursor -> cursor.getString(0)!! }) { + override fun addListener(listener: Listener) { + driver.addListener("post", listener = listener) + } + + override fun removeListener(listener: Listener) { + driver.removeListener("post", listener = listener) + } + + override fun execute(mapper: (SqlCursor) -> QueryResult): QueryResult = + driver.executeQuery(null, "SELECT id FROM POST", mapper, 0, null) + + override fun toString(): String = "Post.sq:posts" +} diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/di/DataComponent.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/di/DataComponent.kt index 39afd5746..f6693f05c 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/di/DataComponent.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/di/DataComponent.kt @@ -15,12 +15,14 @@ */ package dev.sasikanth.rss.reader.di +import app.cash.sqldelight.db.AfterVersion import app.cash.sqldelight.db.SqlDriver import dev.sasikanth.rss.reader.database.Bookmark import dev.sasikanth.rss.reader.database.DateAdapter import dev.sasikanth.rss.reader.database.Feed import dev.sasikanth.rss.reader.database.Post import dev.sasikanth.rss.reader.database.ReaderDatabase +import dev.sasikanth.rss.reader.database.migrations.SQLCodeMigrations import dev.sasikanth.rss.reader.di.scopes.AppScope import me.tatarka.inject.annotations.Provides @@ -46,6 +48,8 @@ internal interface DataComponent : SqlDriverPlatformComponent, DataStorePlatform ) } + @Provides @AppScope fun providesMigrations(): Array = SQLCodeMigrations.migrations() + @Provides fun providesFeedQueries(database: ReaderDatabase) = database.feedQueries @Provides fun providesPostQueries(database: ReaderDatabase) = database.postQueries diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/HomeEvent.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/HomeEvent.kt index 59df2e1e0..aa6ab19b1 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/HomeEvent.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/HomeEvent.kt @@ -50,5 +50,5 @@ sealed interface HomeEvent { data object SettingsClicked : HomeEvent - data class TogglePostReadStatus(val postLink: String, val postRead: Boolean) : HomeEvent + data class TogglePostReadStatus(val postId: String, val postRead: Boolean) : HomeEvent } diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/HomePresenter.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/HomePresenter.kt index eb9bb9765..40ad1df90 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/HomePresenter.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/HomePresenter.kt @@ -73,7 +73,7 @@ internal typealias HomePresenterFactory = openSearch: () -> Unit, openBookmarks: () -> Unit, openSettings: () -> Unit, - openPost: (String) -> Unit, + openPost: (PostWithMetadata) -> Unit, openFeedInfo: (String) -> Unit, ) -> HomePresenter @@ -89,7 +89,7 @@ class HomePresenter( @Assisted private val openSearch: () -> Unit, @Assisted private val openBookmarks: () -> Unit, @Assisted private val openSettings: () -> Unit, - @Assisted private val openPost: (postLink: String) -> Unit, + @Assisted private val openPost: (post: PostWithMetadata) -> Unit, @Assisted private val openFeedInfo: (String) -> Unit, ) : ComponentContext by componentContext { @@ -137,7 +137,7 @@ class HomePresenter( is HomeEvent.SearchClicked -> openSearch() is HomeEvent.BookmarksClicked -> openBookmarks() is HomeEvent.SettingsClicked -> openSettings() - is HomeEvent.OnPostClicked -> openPost(event.post.link) + is HomeEvent.OnPostClicked -> openPost(event.post) else -> { // no-op } @@ -192,14 +192,12 @@ class HomePresenter( } is HomeEvent.OnPostSourceClicked -> postSourceClicked(event.feedLink) is HomeEvent.OnPostsTypeChanged -> onPostsTypeChanged(event.postsType) - is HomeEvent.TogglePostReadStatus -> togglePostReadStatus(event.postLink, event.postRead) + is HomeEvent.TogglePostReadStatus -> togglePostReadStatus(event.postId, event.postRead) } } - private fun togglePostReadStatus(postLink: String, postRead: Boolean) { - coroutineScope.launch { - rssRepository.updatePostReadStatus(read = !postRead, link = postLink) - } + private fun togglePostReadStatus(postId: String, postRead: Boolean) { + coroutineScope.launch { rssRepository.updatePostReadStatus(read = !postRead, id = postId) } } private fun onPostsTypeChanged(postsType: PostsType) { @@ -215,7 +213,7 @@ class HomePresenter( private fun onPostBookmarkClicked(post: PostWithMetadata) { coroutineScope.launch { - rssRepository.updateBookmarkStatus(bookmarked = !post.bookmarked, link = post.link) + rssRepository.updateBookmarkStatus(bookmarked = !post.bookmarked, id = post.id) } } diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/ui/FeaturedSection.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/ui/FeaturedSection.kt index 13241d6b7..4a10ea2c3 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/ui/FeaturedSection.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/ui/FeaturedSection.kt @@ -181,7 +181,7 @@ internal fun FeaturedSection( onBookmarkClick = { onPostBookmarkClick(featuredPost) }, onCommentsClick = { onPostCommentsClick(featuredPost.commentsLink!!) }, onSourceClick = { onPostSourceClick(featuredPost.feedLink) }, - onTogglePostReadClick = { onTogglePostReadClick(featuredPost.link, featuredPost.read) } + onTogglePostReadClick = { onTogglePostReadClick(featuredPost.id, featuredPost.read) } ) } } diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/ui/HomeScreen.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/ui/HomeScreen.kt index cebadd9b4..be1d7cd54 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/ui/HomeScreen.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/ui/HomeScreen.kt @@ -170,8 +170,8 @@ internal fun HomeScreen(homePresenter: HomePresenter, modifier: Modifier = Modif homePresenter.dispatch(HomeEvent.OnPostSourceClicked(feedLink)) }, onNoFeedsSwipeUp = { coroutineScope.launch { bottomSheetState.expand() } }, - onTogglePostReadStatus = { postLink, postRead -> - homePresenter.dispatch(HomeEvent.TogglePostReadStatus(postLink, postRead)) + onTogglePostReadStatus = { postId, postRead -> + homePresenter.dispatch(HomeEvent.TogglePostReadStatus(postId, postRead)) } ) }, diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/ui/PostList.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/ui/PostList.kt index 6909cbf61..0d0249a48 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/ui/PostList.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/ui/PostList.kt @@ -114,7 +114,7 @@ internal fun PostsList( onPostBookmarkClick = { onPostBookmarkClick(post) }, onPostCommentsClick = { onPostCommentsClick(post.commentsLink!!) }, onPostSourceClick = { onPostSourceClick(post.feedLink) }, - togglePostReadClick = { onTogglePostReadClick(post.link, post.read) } + togglePostReadClick = { onTogglePostReadClick(post.id, post.read) } ) } else { Box(Modifier.requiredHeight(132.dp)) diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/reader/ReaderEvent.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/reader/ReaderEvent.kt index 43b96fe65..f2ee8669d 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/reader/ReaderEvent.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/reader/ReaderEvent.kt @@ -18,7 +18,7 @@ package dev.sasikanth.rss.reader.reader sealed interface ReaderEvent { - data class Init(val postLink: String) : ReaderEvent + data class Init(val postId: String) : ReaderEvent data object BackClicked : ReaderEvent diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/reader/ReaderPresenter.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/reader/ReaderPresenter.kt index c7969590d..f231f6110 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/reader/ReaderPresenter.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/reader/ReaderPresenter.kt @@ -44,7 +44,7 @@ import me.tatarka.inject.annotations.Inject internal typealias ReaderPresenterFactory = ( - postLink: String, + postId: String, ComponentContext, goBack: () -> Unit, ) -> ReaderPresenter @@ -54,7 +54,7 @@ class ReaderPresenter( dispatchersProvider: DispatchersProvider, private val rssRepository: RssRepository, private val postSourceFetcher: PostSourceFetcher, - @Assisted private val postLink: String, + @Assisted private val postId: String, @Assisted componentContext: ComponentContext, @Assisted private val goBack: () -> Unit ) : ComponentContext by componentContext { @@ -64,13 +64,13 @@ class ReaderPresenter( PresenterInstance( dispatchersProvider = dispatchersProvider, rssRepository = rssRepository, - postLink = postLink, + postId = postId, postSourceFetcher = postSourceFetcher ) } init { - lifecycle.doOnCreate { presenterInstance.dispatch(ReaderEvent.Init(postLink)) } + lifecycle.doOnCreate { presenterInstance.dispatch(ReaderEvent.Init(postId)) } lifecycle.doOnDestroy { presenterInstance.dispatch(ReaderEvent.MarkPostAsRead) } } @@ -90,47 +90,47 @@ class ReaderPresenter( private class PresenterInstance( private val dispatchersProvider: DispatchersProvider, private val rssRepository: RssRepository, - private val postLink: String, + private val postId: String, private val postSourceFetcher: PostSourceFetcher, ) : InstanceKeeper.Instance { private val coroutineScope = CoroutineScope(SupervisorJob() + dispatchersProvider.main) - private val _state = MutableStateFlow(ReaderState.default(postLink)) + private val _state = MutableStateFlow(ReaderState.default(postId)) val state: StateFlow = _state.stateIn( scope = coroutineScope, started = SharingStarted.WhileSubscribed(5000), - initialValue = ReaderState.default(postLink) + initialValue = ReaderState.default(postId) ) fun dispatch(event: ReaderEvent) { when (event) { - is ReaderEvent.Init -> init(event.postLink) + is ReaderEvent.Init -> init(event.postId) ReaderEvent.BackClicked -> { /* no-op */ } - ReaderEvent.TogglePostBookmark -> togglePostBookmark(postLink) + ReaderEvent.TogglePostBookmark -> togglePostBookmark(postId) ReaderEvent.ArticleShortcutClicked -> articleShortcutClicked() - ReaderEvent.MarkPostAsRead -> markPostAsRead(postLink) + ReaderEvent.MarkPostAsRead -> markPostAsRead(postId) } } - private fun markPostAsRead(postLink: String) { - coroutineScope.launch { rssRepository.updatePostReadStatus(read = true, link = postLink) } + private fun markPostAsRead(postId: String) { + coroutineScope.launch { rssRepository.updatePostReadStatus(read = true, id = postId) } } - private fun togglePostBookmark(postLink: String) { + private fun togglePostBookmark(postId: String) { coroutineScope.launch { val isBookmarked = state.value.isBookmarked ?: false - rssRepository.updateBookmarkStatus(bookmarked = !isBookmarked, link = postLink) + rssRepository.updateBookmarkStatus(bookmarked = !isBookmarked, id = postId) _state.update { it.copy(isBookmarked = !isBookmarked) } } } - private fun init(postLink: String) { + private fun init(postId: String) { coroutineScope.launch { - val post = rssRepository.post(postLink) + val post = rssRepository.post(postId) val feed = rssRepository.feedBlocking(post.feedLink) _state.update { @@ -151,9 +151,10 @@ class ReaderPresenter( } } - private suspend fun extractArticleHtmlContent(postLink: String, content: String): String { + private suspend fun extractArticleHtmlContent(postId: String, content: String): String { + val post = rssRepository.post(postId) val article = - withContext(dispatchersProvider.io) { Readability(postLink, content).parse() } + withContext(dispatchersProvider.io) { Readability(post.id, content).parse() } ?: return content val articleContent = article.content @@ -178,18 +179,18 @@ class ReaderPresenter( private suspend fun loadRssContent() { _state.update { it.copy(postMode = InProgress) } - val post = rssRepository.post(postLink) + val post = rssRepository.post(postId) val postContent = post.rawContent ?: post.description - val htmlContent = extractArticleHtmlContent(postLink, postContent) + val htmlContent = extractArticleHtmlContent(postId, postContent) _state.update { it.copy(content = htmlContent, postMode = RssContent) } } private suspend fun loadSourceArticle() { _state.update { it.copy(postMode = InProgress) } - val content = postSourceFetcher.fetch(postLink) + val content = postSourceFetcher.fetch(postId) if (content.isSuccess) { - val htmlContent = extractArticleHtmlContent(postLink, content.getOrThrow()) + val htmlContent = extractArticleHtmlContent(postId, content.getOrThrow()) _state.update { it.copy(content = htmlContent) } } else { loadRssContent() diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/repository/RssRepository.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/repository/RssRepository.kt index 2d2865583..1a0406321 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/repository/RssRepository.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/repository/RssRepository.kt @@ -33,6 +33,7 @@ import dev.sasikanth.rss.reader.database.PostSearchFTSQueries import dev.sasikanth.rss.reader.di.scopes.AppScope import dev.sasikanth.rss.reader.search.SearchSortOrder import dev.sasikanth.rss.reader.util.DispatchersProvider +import dev.sasikanth.rss.reader.utils.nameBasedUuidOf import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.joinAll @@ -89,6 +90,7 @@ class RssRepository( feedPayload.posts.forEach { post -> if (post.date > feedLastCleanUpAtEpochMilli) { postQueries.upsert( + id = nameBasedUuidOf(post.link).toString(), title = post.title, description = post.description, imageUrl = post.imageUrl, @@ -195,16 +197,16 @@ class RssRepository( ) } - suspend fun updateBookmarkStatus(bookmarked: Boolean, link: String) { - withContext(ioDispatcher) { postQueries.updateBookmarkStatus(bookmarked, link) } + suspend fun updateBookmarkStatus(bookmarked: Boolean, id: String) { + withContext(ioDispatcher) { postQueries.updateBookmarkStatus(bookmarked = bookmarked, id = id) } } - suspend fun updatePostReadStatus(read: Boolean, link: String) { - withContext(ioDispatcher) { postQueries.updateReadStatus(read, link) } + suspend fun updatePostReadStatus(read: Boolean, id: String) { + withContext(ioDispatcher) { postQueries.updateReadStatus(read, id) } } - suspend fun deleteBookmark(link: String) { - withContext(ioDispatcher) { bookmarkQueries.deleteBookmark(link) } + suspend fun deleteBookmark(id: String) { + withContext(ioDispatcher) { bookmarkQueries.deleteBookmark(id) } } fun allFeeds( @@ -479,8 +481,8 @@ class RssRepository( withContext(ioDispatcher) { postQueries.markPostsInFeedAsRead(feedLink, postsAfter) } } - suspend fun post(link: String): Post { - return withContext(ioDispatcher) { postQueries.post(link, ::Post).executeAsOne() } + suspend fun post(postId: String): Post { + return withContext(ioDispatcher) { postQueries.post(postId, ::Post).executeAsOne() } } suspend fun updateFeedAlwaysFetchSource(feedLink: String, newValue: Boolean) { @@ -489,10 +491,6 @@ class RssRepository( } } - suspend fun feedsCount(): Long { - return withContext(ioDispatcher) { feedQueries.count().executeAsOne() } - } - private fun sanitizeSearchQuery(searchQuery: String): String { return searchQuery.replace(Regex.fromLiteral("\""), "\"\"").run { "\"$this\"" } } diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/search/SearchEvent.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/search/SearchEvent.kt index 90d9bf949..a59906db9 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/search/SearchEvent.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/search/SearchEvent.kt @@ -36,5 +36,5 @@ internal sealed interface SearchEvent { data class OnPostClicked(val post: PostWithMetadata) : SearchEvent - data class TogglePostReadStatus(val postLink: String, val postRead: Boolean) : SearchEvent + data class TogglePostReadStatus(val postId: String, val postRead: Boolean) : SearchEvent } diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/search/SearchPresenter.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/search/SearchPresenter.kt index 1a6c91c59..fa616ee11 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/search/SearchPresenter.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/search/SearchPresenter.kt @@ -54,7 +54,7 @@ internal typealias SearchPresentFactory = ( ComponentContext, goBack: () -> Unit, - openPost: (String) -> Unit, + openPost: (PostWithMetadata) -> Unit, ) -> SearchPresenter @Inject @@ -63,7 +63,7 @@ class SearchPresenter( dispatchersProvider: DispatchersProvider, @Assisted componentContext: ComponentContext, @Assisted private val goBack: () -> Unit, - @Assisted private val openPost: (postLink: String) -> Unit, + @Assisted private val openPost: (post: PostWithMetadata) -> Unit, ) : ComponentContext by componentContext { private val presenterInstance = @@ -85,7 +85,7 @@ class SearchPresenter( internal fun dispatch(event: SearchEvent) { when (event) { - is SearchEvent.OnPostClicked -> openPost(event.post.link) + is SearchEvent.OnPostClicked -> openPost(event.post) SearchEvent.BackClicked -> goBack() else -> { /* no-op */ @@ -154,19 +154,17 @@ class SearchPresenter( is SearchEvent.OnPostClicked -> { // no-op } - is SearchEvent.TogglePostReadStatus -> togglePostReadStatus(event.postLink, event.postRead) + is SearchEvent.TogglePostReadStatus -> togglePostReadStatus(event.postId, event.postRead) } } - private fun togglePostReadStatus(postLink: String, postRead: Boolean) { - coroutineScope.launch { - rssRepository.updatePostReadStatus(read = !postRead, link = postLink) - } + private fun togglePostReadStatus(postId: String, postRead: Boolean) { + coroutineScope.launch { rssRepository.updatePostReadStatus(read = !postRead, id = postId) } } private fun onPostBookmarkClick(post: PostWithMetadata) { coroutineScope.launch { - rssRepository.updateBookmarkStatus(bookmarked = !post.bookmarked, link = post.link) + rssRepository.updateBookmarkStatus(bookmarked = !post.bookmarked, id = post.id) } } diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/search/ui/SearchScreen.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/search/ui/SearchScreen.kt index f0aef05d2..5c2b6c7eb 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/search/ui/SearchScreen.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/search/ui/SearchScreen.kt @@ -148,7 +148,7 @@ internal fun SearchScreen(searchPresenter: SearchPresenter, modifier: Modifier = // no-op }, togglePostReadClick = { - searchPresenter.dispatch(SearchEvent.TogglePostReadStatus(post.link, post.read)) + searchPresenter.dispatch(SearchEvent.TogglePostReadStatus(post.id, post.read)) } ) diff --git a/shared/src/commonMain/sqldelight/databases/13.db b/shared/src/commonMain/sqldelight/databases/13.db new file mode 100644 index 0000000000000000000000000000000000000000..b29cc709819eafdac9a4680b472e70f6c4a13075 GIT binary patch literal 86016 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 :after; post: SELECT * FROM post -WHERE post.link = :link; +WHERE post.id = :id; hasPost: -SELECT EXISTS(SELECT 1 FROM post WHERE link = :link); +SELECT EXISTS(SELECT 1 FROM post WHERE id = :id); + +allPostsBlocking: +SELECT * FROM post; + +updatePostId: +UPDATE post SET id = :newId WHERE id = :oldId; diff --git a/shared/src/commonMain/sqldelight/dev/sasikanth/rss/reader/database/PostSearchFTS.sq b/shared/src/commonMain/sqldelight/dev/sasikanth/rss/reader/database/PostSearchFTS.sq index 5010a71a9..574bec50e 100644 --- a/shared/src/commonMain/sqldelight/dev/sasikanth/rss/reader/database/PostSearchFTS.sq +++ b/shared/src/commonMain/sqldelight/dev/sasikanth/rss/reader/database/PostSearchFTS.sq @@ -1,26 +1,27 @@ CREATE VIRTUAL TABLE IF NOT EXISTS post_search USING FTS5( - title TEXT NOT NULL, - description TEXT NOT NULL, - link TEXT NOT NULL PRIMARY KEY UNINDEXED, - tokenize="trigram" + id TEXT NOT NULL PRIMARY KEY, + title TEXT NOT NULL, + description TEXT NOT NULL, + link TEXT NOT NULL, + tokenize="trigram" ); CREATE TRIGGER IF NOT EXISTS post_search_fts_BEFORE_DELETE BEFORE DELETE ON post -BEGIN DELETE FROM post_search WHERE link = old.link; +BEGIN DELETE FROM post_search WHERE id = old.id; END; CREATE TRIGGER IF NOT EXISTS post_search_fts_AFTER_UPDATE AFTER UPDATE ON post -BEGIN UPDATE OR IGNORE post_search SET title = new.title, description = new.description WHERE link = new.link; +BEGIN UPDATE OR IGNORE post_search SET title = new.title, description = new.description WHERE id = new.id; END; CREATE TRIGGER IF NOT EXISTS post_search_fts_AFTER_INSERT AFTER INSERT ON post -BEGIN INSERT OR IGNORE INTO post_search(title, description, link) VALUES (new.title, new.description, new.link); +BEGIN INSERT OR IGNORE INTO post_search(id, title, description, link) VALUES (new.id, new.title, new.description, new.link); END; countSearchResults: @@ -28,6 +29,7 @@ SELECT COUNT(*) FROM post_search WHERE post_search MATCH :searchQuery; search: SELECT + post.id, post_search.title, post_search.description, post.imageUrl, @@ -40,7 +42,7 @@ SELECT post.commentsLink, post.read FROM post_search -INNER JOIN post ON post.link == post_search.link +INNER JOIN post ON post.id == post_search.id INNER JOIN feed ON post.feedLink == feed.link WHERE post_search MATCH :searchQuery ORDER BY diff --git a/shared/src/commonMain/sqldelight/migrations/12.sqm b/shared/src/commonMain/sqldelight/migrations/12.sqm new file mode 100644 index 000000000..08b0da04a --- /dev/null +++ b/shared/src/commonMain/sqldelight/migrations/12.sqm @@ -0,0 +1,140 @@ +import kotlin.Boolean; +import kotlinx.datetime.Instant; + +-- Start: Migrate post table to include id column -- +DROP INDEX post_feed_link_index; + +ALTER TABLE post ADD COLUMN id TEXT NOT NULL DEFAULT ''; +UPDATE post SET id = link; + +CREATE TABLE post_temp( + id TEXT NOT NULL PRIMARY KEY, + title TEXT NOT NULL, + description TEXT NOT NULL, + imageUrl TEXT, + date INTEGER AS Instant NOT NULL, + feedLink TEXT NOT NULL, + link TEXT NOT NULL, + bookmarked INTEGER AS Boolean NOT NULL DEFAULT 0, + commentsLink TEXT DEFAULT NULL, + read INTEGER AS Boolean NOT NULL DEFAULT 0, + rawContent TEXT, + FOREIGN KEY(feedLink) REFERENCES feed(link) ON DELETE CASCADE +); + +INSERT INTO post_temp +SELECT + id, + title, + description, + imageUrl, + date, + feedLink, + link, + bookmarked, + commentsLink, + read, + rawContent +FROM post; + +DROP TABLE post; + +ALTER TABLE post_temp RENAME TO post; + +CREATE INDEX post_feed_link_index ON post(feedLink); +-- End: Migrate post table to include id column -- + +-- Start: Migrate post search table to include id column -- +DROP TABLE post_search; + +CREATE VIRTUAL TABLE IF NOT EXISTS post_search USING FTS5( + id TEXT NOT NULL PRIMARY KEY, + title TEXT NOT NULL, + description TEXT NOT NULL, + link TEXT NOT NULL, + tokenize="trigram" +); + +INSERT INTO post_search SELECT id, title, description, link FROM post; + +CREATE TRIGGER IF NOT EXISTS +post_search_fts_BEFORE_DELETE +BEFORE DELETE ON post +BEGIN DELETE FROM post_search WHERE id = old.id; +END; + +CREATE TRIGGER IF NOT EXISTS +post_search_fts_AFTER_UPDATE +AFTER UPDATE ON post +BEGIN UPDATE OR IGNORE post_search SET title = new.title, description = new.description WHERE id = new.id; +END; + +CREATE TRIGGER IF NOT EXISTS +post_search_fts_AFTER_INSERT +AFTER INSERT ON post +BEGIN INSERT OR IGNORE INTO post_search(id, title, description, link) VALUES (new.id, new.title, new.description, new.link); +END; +-- End: Migrate post search table to include id column -- + +-- Start: Migrate bookmark table to include id column -- +ALTER TABLE bookmark ADD COLUMN id TEXT NOT NULL DEFAULT ''; +UPDATE bookmark SET id = link; + +CREATE TABLE bookmark_temp( + id TEXT NOT NULL PRIMARY KEY, + title TEXT NOT NULL, + description TEXT NOT NULL, + imageUrl TEXT, + date INTEGER AS Instant NOT NULL, + link TEXT NOT NULL, + bookmarked INTEGER AS Boolean NOT NULL DEFAULT 0, + feedName TEXT NOT NULL, + feedIcon TEXT NOT NULL, + feedLink TEXT NOT NULL, + commentsLink TEXT DEFAULT NULL, + read INTEGER AS Boolean NOT NULL DEFAULT 0 +); + +INSERT INTO bookmark_temp +SELECT + id, + title, + description, + imageUrl, + date, + link, + bookmarked, + feedName, + feedIcon, + feedLink, + commentsLink, + read +FROM bookmark; + +DROP TABLE bookmark; + +ALTER TABLE bookmark_temp RENAME TO bookmark; + +CREATE TRIGGER IF NOT EXISTS +post_bookmarked +AFTER UPDATE OF bookmarked ON post WHEN new.bookmarked == 1 +BEGIN + INSERT OR REPLACE INTO bookmark(id, title, description, imageUrl, date, link, bookmarked, commentsLink, feedName, feedIcon, feedLink, read) + SELECT new.id, new.title, new.description, new.imageUrl, new.date, new.link, new.bookmarked, new.commentsLink, feed.name, feed.icon, feed.link, new.read + FROM feed WHERE link == new.feedLink; +END; + +CREATE TRIGGER IF NOT EXISTS +post_unbookmarked +AFTER UPDATE OF bookmarked ON post WHEN new.bookmarked == 0 +BEGIN DELETE FROM bookmark WHERE id = new.id; +END; + +CREATE TRIGGER IF NOT EXISTS +post_content_update +AFTER UPDATE OF title, description, imageUrl, date, read ON post WHEN new.bookmarked == 1 +BEGIN + UPDATE OR IGNORE bookmark SET title = new.title, description = new.description, imageUrl = new.imageUrl, date = new.date, commentsLink = new.commentsLink, read = new.read + WHERE id = new.id; +END; +-- End: Migrate bookmark table to include id column -- diff --git a/shared/src/iosMain/kotlin/dev/sasikanth/rss/reader/database/DriverFactory.kt b/shared/src/iosMain/kotlin/dev/sasikanth/rss/reader/database/DriverFactory.kt index 1edc8eb97..f0f38ad54 100644 --- a/shared/src/iosMain/kotlin/dev/sasikanth/rss/reader/database/DriverFactory.kt +++ b/shared/src/iosMain/kotlin/dev/sasikanth/rss/reader/database/DriverFactory.kt @@ -15,6 +15,7 @@ */ package dev.sasikanth.rss.reader.database +import app.cash.sqldelight.db.AfterVersion import app.cash.sqldelight.db.SqlDriver import app.cash.sqldelight.driver.native.NativeSqliteDriver import app.cash.sqldelight.driver.native.wrapConnection @@ -24,7 +25,7 @@ import me.tatarka.inject.annotations.Inject @Inject @AppScope -actual class DriverFactory { +actual class DriverFactory(private val codeMigrations: Array) { actual fun createDriver(): SqlDriver { return NativeSqliteDriver( @@ -34,10 +35,15 @@ actual class DriverFactory { create = { connection -> wrapConnection(connection) { ReaderDatabase.Schema.create(it) } }, upgrade = { connection, oldVersion, newVersion -> wrapConnection(connection) { - ReaderDatabase.Schema.migrate(it, oldVersion.toLong(), newVersion.toLong()) + ReaderDatabase.Schema.migrate( + driver = it, + oldVersion = oldVersion.toLong(), + newVersion = newVersion.toLong(), + callbacks = codeMigrations + ) } }, - extendedConfig = DatabaseConfiguration.Extended(foreignKeyConstraints = true) + extendedConfig = DatabaseConfiguration.Extended(foreignKeyConstraints = true), ) ) } From fded8ec579611ac7a76965bbdfbfe3a9bdeb29e9 Mon Sep 17 00:00:00 2001 From: Sasikanth Miriyampalli Date: Sat, 13 Apr 2024 07:32:00 +0530 Subject: [PATCH 2/8] Fix code migration for converting post link ids to UUID Missed migrating post_search and bookmarks table --- .../database/migrations/SQLCodeMigrations.kt | 38 +++++++++++++------ 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/database/migrations/SQLCodeMigrations.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/database/migrations/SQLCodeMigrations.kt index 0906a8f7c..a092fc12f 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/database/migrations/SQLCodeMigrations.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/database/migrations/SQLCodeMigrations.kt @@ -32,18 +32,34 @@ object SQLCodeMigrations { private fun afterVersion12(): AfterVersion { return AfterVersion(12) { driver -> val ids = PostsIdsQuery(driver).executeAsList() - ids.forEach { id -> - driver.execute( - identifier = null, - sql = "UPDATE post SET id = ? WHERE id = ?", - parameters = 2, - ) { - bindString(0, nameBasedUuidOf(id).toString()) - bindString(1, id) - } - } + ids.forEach { id -> migratePostLinkIdsToUuid(driver, id) } } } + + private fun migratePostLinkIdsToUuid(driver: SqlDriver, oldPostId: String) { + val newPostId = nameBasedUuidOf(oldPostId).toString() + + driver.execute( + identifier = null, + sql = "UPDATE post SET id = '$newPostId' WHERE id = '$oldPostId'", + parameters = 0, + binders = null + ) + + driver.execute( + identifier = null, + sql = "UPDATE post_search SET id = '$newPostId' WHERE id = '$oldPostId'", + parameters = 0, + binders = null + ) + + driver.execute( + identifier = null, + sql = "UPDATE bookmark SET id = '$newPostId' WHERE id = '$oldPostId'", + parameters = 0, + binders = null + ) + } } private class PostsIdsQuery( @@ -58,7 +74,7 @@ private class PostsIdsQuery( } override fun execute(mapper: (SqlCursor) -> QueryResult): QueryResult = - driver.executeQuery(null, "SELECT id FROM POST", mapper, 0, null) + driver.executeQuery(null, "SELECT id FROM post", mapper, 0, null) override fun toString(): String = "Post.sq:posts" } From a90819ab0adb1852572e66280afedebc717c9a3f Mon Sep 17 00:00:00 2001 From: Sasikanth Miriyampalli Date: Sat, 13 Apr 2024 07:32:17 +0530 Subject: [PATCH 3/8] Remove unnecessary post queries --- .../sqldelight/dev/sasikanth/rss/reader/database/Post.sq | 6 ------ 1 file changed, 6 deletions(-) diff --git a/shared/src/commonMain/sqldelight/dev/sasikanth/rss/reader/database/Post.sq b/shared/src/commonMain/sqldelight/dev/sasikanth/rss/reader/database/Post.sq index ee67e7642..8bc7bdeac 100644 --- a/shared/src/commonMain/sqldelight/dev/sasikanth/rss/reader/database/Post.sq +++ b/shared/src/commonMain/sqldelight/dev/sasikanth/rss/reader/database/Post.sq @@ -122,9 +122,3 @@ WHERE post.id = :id; hasPost: SELECT EXISTS(SELECT 1 FROM post WHERE id = :id); - -allPostsBlocking: -SELECT * FROM post; - -updatePostId: -UPDATE post SET id = :newId WHERE id = :oldId; From 3284b166adc80f865b230e8c4c98050dfaf70b5d Mon Sep 17 00:00:00 2001 From: Sasikanth Miriyampalli Date: Sat, 13 Apr 2024 14:09:44 +0530 Subject: [PATCH 4/8] Add unique id column (primary key) to feed table (#441) * Update feed table to add unique feed ID column * Fix missing ids in `FeedsOpmlTest` --- .../rss/reader/core/model/local/Feed.kt | 3 +- .../rss/reader/core/model/local/Post.kt | 6 +- .../core/model/local/PostWithMetadata.kt | 6 +- .../sasikanth/rss/reader/app/AppPresenter.kt | 4 +- .../reader/bookmarks/BookmarksPresenter.kt | 4 +- .../database/migrations/SQLCodeMigrations.kt | 57 ++++- .../sasikanth/rss/reader/feed/FeedEvent.kt | 6 +- .../rss/reader/feed/FeedPresenter.kt | 30 +-- .../rss/reader/feed/ui/FeedInfoBottomSheet.kt | 16 +- .../sasikanth/rss/reader/feeds/FeedsEvent.kt | 2 +- .../rss/reader/feeds/FeedsPresenter.kt | 12 +- .../feeds/ui/BottomSheetCollapsedContent.kt | 2 +- .../feeds/ui/BottomSheetExpandedContent.kt | 9 +- .../sasikanth/rss/reader/home/HomeEvent.kt | 2 +- .../rss/reader/home/HomePresenter.kt | 12 +- .../rss/reader/home/ui/FeaturedSection.kt | 2 +- .../rss/reader/home/ui/HomeScreen.kt | 4 +- .../sasikanth/rss/reader/home/ui/PostList.kt | 2 +- .../rss/reader/reader/ReaderPresenter.kt | 2 +- .../rss/reader/repository/RssRepository.kt | 83 +++---- .../src/commonMain/sqldelight/databases/14.db | Bin 0 -> 86016 bytes .../sasikanth/rss/reader/database/Bookmark.sq | 14 +- .../dev/sasikanth/rss/reader/database/Feed.sq | 44 ++-- .../rss/reader/database/FeedSearchFTS.sq | 24 ++- .../dev/sasikanth/rss/reader/database/Post.sq | 48 ++--- .../rss/reader/database/PostSearchFTS.sq | 10 +- .../commonMain/sqldelight/migrations/13.sqm | 204 ++++++++++++++++++ .../rss/reader/opml/FeedsOpmlTest.kt | 2 + 28 files changed, 440 insertions(+), 170 deletions(-) create mode 100644 shared/src/commonMain/sqldelight/databases/14.db create mode 100644 shared/src/commonMain/sqldelight/migrations/13.sqm diff --git a/core/model/src/commonMain/kotlin/dev/sasikanth/rss/reader/core/model/local/Feed.kt b/core/model/src/commonMain/kotlin/dev/sasikanth/rss/reader/core/model/local/Feed.kt index 08eb6ba74..7012feb94 100644 --- a/core/model/src/commonMain/kotlin/dev/sasikanth/rss/reader/core/model/local/Feed.kt +++ b/core/model/src/commonMain/kotlin/dev/sasikanth/rss/reader/core/model/local/Feed.kt @@ -20,12 +20,13 @@ import kotlinx.datetime.Instant @Immutable data class Feed( + val id: String, val name: String, val icon: String, val description: String, + val link: String, val homepageLink: String, val createdAt: Instant, - val link: String, val pinnedAt: Instant?, val lastCleanUpAt: Instant? = null, val numberOfUnreadPosts: Long = 0L, diff --git a/core/model/src/commonMain/kotlin/dev/sasikanth/rss/reader/core/model/local/Post.kt b/core/model/src/commonMain/kotlin/dev/sasikanth/rss/reader/core/model/local/Post.kt index cd05a7dd0..759b6586b 100644 --- a/core/model/src/commonMain/kotlin/dev/sasikanth/rss/reader/core/model/local/Post.kt +++ b/core/model/src/commonMain/kotlin/dev/sasikanth/rss/reader/core/model/local/Post.kt @@ -20,14 +20,14 @@ import kotlinx.datetime.Instant data class Post( val id: String, + val sourceId: String, val title: String, val description: String, + val rawContent: String?, val imageUrl: String?, val date: Instant, - val feedLink: String, val link: String, - val bookmarked: Boolean, val commentsLink: String?, + val bookmarked: Boolean, val read: Boolean, - val rawContent: String?, ) diff --git a/core/model/src/commonMain/kotlin/dev/sasikanth/rss/reader/core/model/local/PostWithMetadata.kt b/core/model/src/commonMain/kotlin/dev/sasikanth/rss/reader/core/model/local/PostWithMetadata.kt index 8a66b0af5..ebf3237b1 100644 --- a/core/model/src/commonMain/kotlin/dev/sasikanth/rss/reader/core/model/local/PostWithMetadata.kt +++ b/core/model/src/commonMain/kotlin/dev/sasikanth/rss/reader/core/model/local/PostWithMetadata.kt @@ -21,15 +21,15 @@ import kotlinx.datetime.Instant @Immutable data class PostWithMetadata( val id: String, + val sourceId: String, val title: String, val description: String, val imageUrl: String?, val date: Instant, val link: String, + val commentsLink: String?, val bookmarked: Boolean, + val read: Boolean, val feedName: String, val feedIcon: String, - val feedLink: String, - val commentsLink: String?, - val read: Boolean, ) diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/app/AppPresenter.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/app/AppPresenter.kt index 09b1f9386..611423354 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/app/AppPresenter.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/app/AppPresenter.kt @@ -116,7 +116,7 @@ class AppPresenter( is ModalConfig.FeedInfo -> { Modals.FeedInfo( presenter = - feedPresenter(modalConfig.feedLink, componentContext) { modalNavigation.dismiss() } + feedPresenter(modalConfig.feedId, componentContext) { modalNavigation.dismiss() } ) } } @@ -220,6 +220,6 @@ class AppPresenter( @Serializable sealed interface ModalConfig { - @Serializable data class FeedInfo(val feedLink: String) : ModalConfig + @Serializable data class FeedInfo(val feedId: String) : ModalConfig } } diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/bookmarks/BookmarksPresenter.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/bookmarks/BookmarksPresenter.kt index ca82e508c..a7ba712d8 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/bookmarks/BookmarksPresenter.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/bookmarks/BookmarksPresenter.kt @@ -125,7 +125,7 @@ class BookmarksPresenter( ) { coroutineScope.launch { val hasPost = rssRepository.hasPost(post.id) - val hasFeed = rssRepository.hasFeed(post.feedLink) + val hasFeed = rssRepository.hasFeed(post.sourceId) if (hasPost && hasFeed) { openReaderView(post) @@ -137,7 +137,7 @@ class BookmarksPresenter( private fun onPostBookmarkClicked(post: PostWithMetadata) { coroutineScope.launch { - if (rssRepository.hasFeed(post.feedLink)) { + if (rssRepository.hasFeed(post.sourceId)) { rssRepository.updateBookmarkStatus(bookmarked = !post.bookmarked, id = post.id) } else { rssRepository.deleteBookmark(id = post.id) diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/database/migrations/SQLCodeMigrations.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/database/migrations/SQLCodeMigrations.kt index a092fc12f..724d523c6 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/database/migrations/SQLCodeMigrations.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/database/migrations/SQLCodeMigrations.kt @@ -26,7 +26,14 @@ import dev.sasikanth.rss.reader.utils.nameBasedUuidOf object SQLCodeMigrations { fun migrations(): Array { - return arrayOf(afterVersion12()) + return arrayOf(afterVersion12(), afterVersion13()) + } + + private fun afterVersion13(): AfterVersion { + return AfterVersion(13) { driver -> + val feedIds = FeedsIdsQuery(driver).executeAsList() + feedIds.forEach { feedId -> migrateFeedsLinkIdsToUuid(feedId, driver) } + } } private fun afterVersion12(): AfterVersion { @@ -36,6 +43,37 @@ object SQLCodeMigrations { } } + private fun migrateFeedsLinkIdsToUuid(oldFeedId: String, driver: SqlDriver) { + val newFeedId = nameBasedUuidOf(oldFeedId).toString() + driver.execute( + identifier = null, + sql = "UPDATE feed SET id = '$newFeedId' WHERE id = '$oldFeedId'", + parameters = 0, + binders = null + ) + + driver.execute( + identifier = null, + sql = "UPDATE feed_search SET id = '$newFeedId' WHERE id = '$oldFeedId'", + parameters = 0, + binders = null + ) + + driver.execute( + identifier = null, + sql = "UPDATE post SET sourceId = '$newFeedId' WHERE sourceId = '$oldFeedId'", + parameters = 0, + binders = null + ) + + driver.execute( + identifier = null, + sql = "UPDATE bookmark SET sourceId = '$newFeedId' WHERE sourceId = '$oldFeedId'", + parameters = 0, + binders = null + ) + } + private fun migratePostLinkIdsToUuid(driver: SqlDriver, oldPostId: String) { val newPostId = nameBasedUuidOf(oldPostId).toString() @@ -62,6 +100,23 @@ object SQLCodeMigrations { } } +private class FeedsIdsQuery( + private val driver: SqlDriver, +) : Query(mapper = { cursor -> cursor.getString(0)!! }) { + override fun addListener(listener: Listener) { + driver.addListener("feed", listener = listener) + } + + override fun removeListener(listener: Listener) { + driver.removeListener("feed", listener = listener) + } + + override fun execute(mapper: (SqlCursor) -> QueryResult): QueryResult = + driver.executeQuery(null, "SELECT id FROM feed", mapper, 0, null) + + override fun toString(): String = "Feed.sq:feedIds" +} + private class PostsIdsQuery( private val driver: SqlDriver, ) : Query(mapper = { cursor -> cursor.getString(0)!! }) { diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feed/FeedEvent.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feed/FeedEvent.kt index 91715c294..b1c2bdbbf 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feed/FeedEvent.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feed/FeedEvent.kt @@ -24,12 +24,12 @@ sealed interface FeedEvent { data object RemoveFeedClicked : FeedEvent - data class OnFeedNameChanged(val newFeedName: String, val feedLink: String) : FeedEvent + data class OnFeedNameChanged(val newFeedName: String, val feedId: String) : FeedEvent data object DismissSheet : FeedEvent - data class OnAlwaysFetchSourceArticleChanged(val newValue: Boolean, val feedLink: String) : + data class OnAlwaysFetchSourceArticleChanged(val newValue: Boolean, val feedId: String) : FeedEvent - data class OnMarkPostsAsRead(val feedLink: String) : FeedEvent + data class OnMarkPostsAsRead(val feedId: String) : FeedEvent } diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feed/FeedPresenter.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feed/FeedPresenter.kt index 8ac7ddb31..a9c031523 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feed/FeedPresenter.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feed/FeedPresenter.kt @@ -48,7 +48,7 @@ import me.tatarka.inject.annotations.Inject internal typealias FeedPresenterFactory = ( - feedLink: String, + feedId: String, ComponentContext, dismiss: () -> Unit, ) -> FeedPresenter @@ -59,7 +59,7 @@ class FeedPresenter( rssRepository: RssRepository, settingsRepository: SettingsRepository, private val observableSelectedFeed: ObservableSelectedFeed, - @Assisted feedLink: String, + @Assisted feedId: String, @Assisted componentContext: ComponentContext, @Assisted private val dismiss: () -> Unit ) : ComponentContext by componentContext { @@ -70,7 +70,7 @@ class FeedPresenter( dispatchersProvider = dispatchersProvider, rssRepository = rssRepository, settingsRepository = settingsRepository, - feedLink = feedLink, + feedId = feedId, observableSelectedFeed = observableSelectedFeed, ) } @@ -98,7 +98,7 @@ class FeedPresenter( private val dispatchersProvider: DispatchersProvider, private val rssRepository: RssRepository, private val settingsRepository: SettingsRepository, - private val feedLink: String, + private val feedId: String, private val observableSelectedFeed: ObservableSelectedFeed, ) : InstanceKeeper.Instance { @@ -122,14 +122,14 @@ class FeedPresenter( // no-op } FeedEvent.RemoveFeedClicked -> removeFeed() - is FeedEvent.OnFeedNameChanged -> onFeedNameUpdated(event.newFeedName, event.feedLink) + is FeedEvent.OnFeedNameChanged -> onFeedNameUpdated(event.newFeedName, event.feedId) is FeedEvent.OnAlwaysFetchSourceArticleChanged -> - onAlwaysFetchSourceArticleChanged(event.newValue, event.feedLink) - is FeedEvent.OnMarkPostsAsRead -> onMarkPostsAsRead(event.feedLink) + onAlwaysFetchSourceArticleChanged(event.newValue, event.feedId) + is FeedEvent.OnMarkPostsAsRead -> onMarkPostsAsRead(event.feedId) } } - private fun onMarkPostsAsRead(feedLink: String) { + private fun onMarkPostsAsRead(feedId: String) { coroutineScope.launch { val postsType = withContext(dispatchersProvider.io) { settingsRepository.postsType.first() } val postsAfter = @@ -144,21 +144,21 @@ class FeedPresenter( } } - rssRepository.markPostsInFeedAsRead(feedLink = feedLink, postsAfter = postsAfter) + rssRepository.markPostsInFeedAsRead(feedId = feedId, postsAfter = postsAfter) } } - private fun onAlwaysFetchSourceArticleChanged(newValue: Boolean, feedLink: String) { - coroutineScope.launch { rssRepository.updateFeedAlwaysFetchSource(feedLink, newValue) } + private fun onAlwaysFetchSourceArticleChanged(newValue: Boolean, feedId: String) { + coroutineScope.launch { rssRepository.updateFeedAlwaysFetchSource(feedId, newValue) } } - private fun onFeedNameUpdated(newFeedName: String, feedLink: String) { - coroutineScope.launch { rssRepository.updateFeedName(newFeedName, feedLink) } + private fun onFeedNameUpdated(newFeedName: String, feedId: String) { + coroutineScope.launch { rssRepository.updateFeedName(newFeedName, feedId) } } private fun removeFeed() { coroutineScope.launch { - rssRepository.removeFeed(feedLink) + rssRepository.removeFeed(feedId) observableSelectedFeed.clearSelection() effects.emit(FeedEffect.DismissSheet) } @@ -180,7 +180,7 @@ class FeedPresenter( } rssRepository - .feed(feedLink, postsAfter) + .feed(feedId, postsAfter) .onEach { feed -> _state.update { it.copy(feed = feed) } } .catch { // no-op diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feed/ui/FeedInfoBottomSheet.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feed/ui/FeedInfoBottomSheet.kt index 6b4941798..0a7966ad0 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feed/ui/FeedInfoBottomSheet.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feed/ui/FeedInfoBottomSheet.kt @@ -45,7 +45,6 @@ import androidx.compose.foundation.text.selection.LocalTextSelectionColors import androidx.compose.foundation.text.selection.TextSelectionColors import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.rounded.Check import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.CircularProgressIndicator @@ -54,7 +53,6 @@ import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.SheetState -import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable @@ -138,7 +136,7 @@ fun FeedInfoBottomSheet( feed = feed, onFeedNameChange = { newFeedName -> feedPresenter.dispatch( - FeedEvent.OnFeedNameChanged(newFeedName = newFeedName, feedLink = feed.link) + FeedEvent.OnFeedNameChanged(newFeedName = newFeedName, feedId = feed.id) ) } ) @@ -148,15 +146,15 @@ fun FeedInfoBottomSheet( FeedUnreadCount( modifier = Modifier.fillMaxWidth().padding(horizontal = HORIZONTAL_PADDING), numberOfUnreadPosts = feed.numberOfUnreadPosts, - onMarkPostsAsRead = { feedPresenter.dispatch(FeedEvent.OnMarkPostsAsRead(feed.link)) } + onMarkPostsAsRead = { feedPresenter.dispatch(FeedEvent.OnMarkPostsAsRead(feed.id)) } ) Divider(horizontalInsets = HORIZONTAL_PADDING) AlwaysFetchSourceArticleSwitch( feed = feed, - onValueChanged = { newValue, feedLink -> - feedPresenter.dispatch(FeedEvent.OnAlwaysFetchSourceArticleChanged(newValue, feedLink)) + onValueChanged = { newValue, feedId -> + feedPresenter.dispatch(FeedEvent.OnAlwaysFetchSourceArticleChanged(newValue, feedId)) } ) @@ -354,7 +352,7 @@ private fun FeedOptions(feed: Feed, onRemoveFeedClick: () -> Unit, modifier: Mod private fun AlwaysFetchSourceArticleSwitch( feed: Feed, modifier: Modifier = Modifier, - onValueChanged: (newValue: Boolean, feedLink: String) -> Unit + onValueChanged: (newValue: Boolean, feedId: String) -> Unit ) { var checked by remember(feed.alwaysFetchSourceArticle) { mutableStateOf(feed.alwaysFetchSourceArticle) } @@ -363,7 +361,7 @@ private fun AlwaysFetchSourceArticleSwitch( modifier = Modifier.clickable { checked = !checked - onValueChanged(checked, feed.link) + onValueChanged(checked, feed.id) } .padding(vertical = 16.dp, horizontal = HORIZONTAL_PADDING), verticalAlignment = Alignment.CenterVertically @@ -380,7 +378,7 @@ private fun AlwaysFetchSourceArticleSwitch( Switch( modifier = modifier, checked = checked, - onCheckedChange = { newValue -> onValueChanged(newValue, feed.link) } + onCheckedChange = { newValue -> onValueChanged(newValue, feed.id) } ) } } diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/FeedsEvent.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/FeedsEvent.kt index 5c98e20d3..0c8cb92e1 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/FeedsEvent.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/FeedsEvent.kt @@ -29,7 +29,7 @@ sealed interface FeedsEvent { data class OnToggleFeedSelection(val feed: Feed) : FeedsEvent - data class OnFeedNameUpdated(val newFeedName: String, val feedLink: String) : FeedsEvent + data class OnFeedNameUpdated(val newFeedName: String, val feedId: String) : FeedsEvent data class OnFeedPinClicked(val feed: Feed) : FeedsEvent diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/FeedsPresenter.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/FeedsPresenter.kt index f57a504e3..652db61bc 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/FeedsPresenter.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/FeedsPresenter.kt @@ -135,7 +135,7 @@ class FeedsPresenter( FeedsEvent.OnGoBackClicked -> onGoBackClicked() is FeedsEvent.OnDeleteFeed -> onDeleteFeed(event.feed) is FeedsEvent.OnToggleFeedSelection -> onToggleFeedSelection(event.feed) - is FeedsEvent.OnFeedNameUpdated -> onFeedNameUpdated(event.newFeedName, event.feedLink) + is FeedsEvent.OnFeedNameUpdated -> onFeedNameUpdated(event.newFeedName, event.feedId) is FeedsEvent.OnFeedPinClicked -> onFeedPinClicked(event.feed) FeedsEvent.ClearSearchQuery -> clearSearchQuery() is FeedsEvent.SearchQueryChanged -> onSearchQueryChanged(event.searchQuery) @@ -156,7 +156,7 @@ class FeedsPresenter( private fun onFeedClicked(feed: Feed) { coroutineScope.launch { - if (_state.value.selectedFeed?.link != feed.link) { + if (_state.value.selectedFeed?.id != feed.id) { observableSelectedFeed.selectFeed(feed) } @@ -229,14 +229,14 @@ class FeedsPresenter( coroutineScope.launch { rssRepository.toggleFeedPinStatus(feed) } } - private fun onFeedNameUpdated(newFeedName: String, feedLink: String) { - coroutineScope.launch { rssRepository.updateFeedName(newFeedName, feedLink) } + private fun onFeedNameUpdated(newFeedName: String, feedId: String) { + coroutineScope.launch { rssRepository.updateFeedName(newFeedName, feedId) } } private fun onDeleteFeed(feed: Feed) { coroutineScope.launch { - rssRepository.removeFeed(feed.link) - if (_state.value.selectedFeed?.link == feed.link) { + rssRepository.removeFeed(feed.id) + if (_state.value.selectedFeed?.id == feed.id) { observableSelectedFeed.clearSelection() } } diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/ui/BottomSheetCollapsedContent.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/ui/BottomSheetCollapsedContent.kt index 9be0af2dd..96fb6422c 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/ui/BottomSheetCollapsedContent.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/ui/BottomSheetCollapsedContent.kt @@ -82,7 +82,7 @@ internal fun BottomSheetCollapsedContent( badgeCount = feed.numberOfUnreadPosts, iconUrl = feed.icon, canShowUnreadPostsCount = canShowUnreadPostsCount, - selected = selectedFeed?.link == feed.link, + selected = selectedFeed?.id == feed.id, onClick = { onFeedClick(feed) } ) } diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/ui/BottomSheetExpandedContent.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/ui/BottomSheetExpandedContent.kt index 79e3df024..c25275224 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/ui/BottomSheetExpandedContent.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/ui/BottomSheetExpandedContent.kt @@ -77,7 +77,6 @@ import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.dp import app.cash.paging.compose.LazyPagingItems -import app.cash.paging.compose.itemContentType import app.cash.paging.compose.itemKey import dev.sasikanth.rss.reader.components.ContextActionItem import dev.sasikanth.rss.reader.components.ContextActionsBottomBar @@ -351,8 +350,8 @@ private fun LazyGridScope.feedSearchResults( ) { items( count = searchResults.itemCount, - key = searchResults.itemKey { it.link }, - contentType = searchResults.itemContentType { it.link }, + key = searchResults.itemKey { "SearchResult:${it.id}" }, + contentType = { "FeedListItem" }, span = { gridItemSpan } ) { index -> val feed = searchResults[index] @@ -403,7 +402,7 @@ private fun LazyGridScope.allFeeds( items( count = feeds.itemCount, - key = feeds.itemKey { it.link }, + key = feeds.itemKey { it.id }, contentType = { "FeedListItem" }, span = { gridItemSpan } ) { index -> @@ -456,7 +455,7 @@ private fun LazyGridScope.pinnedFeeds( if (isPinnedSectionExpanded) { items( count = pinnedFeeds.itemCount, - key = pinnedFeeds.itemKey { "PinnedFeed:${it.link}" }, + key = pinnedFeeds.itemKey { "PinnedFeed:${it.id}" }, contentType = { "FeedListItem" }, span = { gridItemSpan } ) { index -> diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/HomeEvent.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/HomeEvent.kt index aa6ab19b1..2554f2088 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/HomeEvent.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/HomeEvent.kt @@ -30,7 +30,7 @@ sealed interface HomeEvent { data class OnPostClicked(val post: PostWithMetadata) : HomeEvent - data class OnPostSourceClicked(val feedLink: String) : HomeEvent + data class OnPostSourceClicked(val feedId: String) : HomeEvent data class FeedsSheetStateChanged(val feedsSheetState: BottomSheetValue) : HomeEvent diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/HomePresenter.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/HomePresenter.kt index 40ad1df90..2856d15f0 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/HomePresenter.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/HomePresenter.kt @@ -190,7 +190,7 @@ class HomePresenter( HomeEvent.SettingsClicked -> { /* no-op */ } - is HomeEvent.OnPostSourceClicked -> postSourceClicked(event.feedLink) + is HomeEvent.OnPostSourceClicked -> postSourceClicked(event.feedId) is HomeEvent.OnPostsTypeChanged -> onPostsTypeChanged(event.postsType) is HomeEvent.TogglePostReadStatus -> togglePostReadStatus(event.postId, event.postRead) } @@ -204,9 +204,9 @@ class HomePresenter( coroutineScope.launch { settingsRepository.updatePostsType(postsType) } } - private fun postSourceClicked(feedLink: String) { + private fun postSourceClicked(feedId: String) { coroutineScope.launch { - val feed = rssRepository.feedBlocking(feedLink) + val feed = rssRepository.feedBlocking(feedId) observableSelectedFeed.selectFeed(feed) } } @@ -254,7 +254,7 @@ class HomePresenter( val posts = createPager(config = createPagingConfig(pageSize = 20, enablePlaceholders = true)) { rssRepository.posts( - selectedFeedLink = selectedFeed?.link, + selectedFeedId = selectedFeed?.id, unreadOnly = unreadOnly, after = postsAfter ) @@ -264,7 +264,7 @@ class HomePresenter( rssRepository .featuredPosts( - selectedFeedLink = selectedFeed?.link, + selectedFeedId = selectedFeed?.id, unreadOnly = unreadOnly, after = postsAfter ) @@ -386,7 +386,7 @@ class HomePresenter( try { val selectedFeed = _state.value.selectedFeed if (selectedFeed != null) { - rssRepository.updateFeed(selectedFeed.link) + rssRepository.updateFeed(selectedFeed.id) } else { rssRepository.updateFeeds() } diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/ui/FeaturedSection.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/ui/FeaturedSection.kt index 4a10ea2c3..a07f9e1d9 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/ui/FeaturedSection.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/ui/FeaturedSection.kt @@ -180,7 +180,7 @@ internal fun FeaturedSection( onClick = { onItemClick(featuredPost) }, onBookmarkClick = { onPostBookmarkClick(featuredPost) }, onCommentsClick = { onPostCommentsClick(featuredPost.commentsLink!!) }, - onSourceClick = { onPostSourceClick(featuredPost.feedLink) }, + onSourceClick = { onPostSourceClick(featuredPost.sourceId) }, onTogglePostReadClick = { onTogglePostReadClick(featuredPost.id, featuredPost.read) } ) } diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/ui/HomeScreen.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/ui/HomeScreen.kt index be1d7cd54..b2271d67d 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/ui/HomeScreen.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/ui/HomeScreen.kt @@ -166,8 +166,8 @@ internal fun HomeScreen(homePresenter: HomePresenter, modifier: Modifier = Modif onPostCommentsClick = { commentsLink -> coroutineScope.launch { linkHandler.openLink(commentsLink) } }, - onPostSourceClick = { feedLink -> - homePresenter.dispatch(HomeEvent.OnPostSourceClicked(feedLink)) + onPostSourceClick = { feedId -> + homePresenter.dispatch(HomeEvent.OnPostSourceClicked(feedId)) }, onNoFeedsSwipeUp = { coroutineScope.launch { bottomSheetState.expand() } }, onTogglePostReadStatus = { postId, postRead -> diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/ui/PostList.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/ui/PostList.kt index 0d0249a48..a22cd8d94 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/ui/PostList.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/ui/PostList.kt @@ -113,7 +113,7 @@ internal fun PostsList( onClick = { onPostClicked(post) }, onPostBookmarkClick = { onPostBookmarkClick(post) }, onPostCommentsClick = { onPostCommentsClick(post.commentsLink!!) }, - onPostSourceClick = { onPostSourceClick(post.feedLink) }, + onPostSourceClick = { onPostSourceClick(post.sourceId) }, togglePostReadClick = { onTogglePostReadClick(post.id, post.read) } ) } else { diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/reader/ReaderPresenter.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/reader/ReaderPresenter.kt index f231f6110..e8dcf2bfd 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/reader/ReaderPresenter.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/reader/ReaderPresenter.kt @@ -131,7 +131,7 @@ class ReaderPresenter( private fun init(postId: String) { coroutineScope.launch { val post = rssRepository.post(postId) - val feed = rssRepository.feedBlocking(post.feedLink) + val feed = rssRepository.feedBlocking(post.sourceId) _state.update { it.copy( diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/repository/RssRepository.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/repository/RssRepository.kt index 1a0406321..1f0d2046f 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/repository/RssRepository.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/repository/RssRepository.kt @@ -73,13 +73,16 @@ class RssRepository( is FeedFetchResult.Success -> { return@withContext try { val feedPayload = feedFetchResult.feedPayload + val feedId = nameBasedUuidOf(feedPayload.link).toString() + feedQueries.upsert( name = title ?: feedPayload.name, icon = feedPayload.icon, description = feedPayload.description, homepageLink = feedPayload.homepageLink, createdAt = Clock.System.now(), - link = feedPayload.link + link = feedPayload.link, + id = feedId ) postQueries.transaction { @@ -91,13 +94,13 @@ class RssRepository( if (post.date > feedLastCleanUpAtEpochMilli) { postQueries.upsert( id = nameBasedUuidOf(post.link).toString(), + sourceId = feedId, title = post.title, description = post.description, imageUrl = post.imageUrl, date = Instant.fromEpochMilliseconds(post.date), link = post.link, commnetsLink = post.commentsLink, - feedLink = feedPayload.link, rawContent = post.rawContent ) } @@ -142,9 +145,9 @@ class RssRepository( results.flatten().joinAll() } - suspend fun updateFeed(selectedFeedLink: String) { + suspend fun updateFeed(selectedFeedId: String) { withContext(ioDispatcher) { - val feed = feedQueries.feed(selectedFeedLink).executeAsOneOrNull() + val feed = feedQueries.feed(selectedFeedId).executeAsOneOrNull() if (feed != null) { addFeed(feedLink = feed.link, transformUrl = false, feedLastCleanUpAt = feed.lastCleanUpAt) } @@ -152,13 +155,13 @@ class RssRepository( } fun featuredPosts( - selectedFeedLink: String?, + selectedFeedId: String?, unreadOnly: Boolean? = null, after: Instant = Instant.DISTANT_PAST ): Flow> { return postQueries .featuredPosts( - feedLink = selectedFeedLink, + sourceId = selectedFeedId, unreadOnly = unreadOnly, postsAfter = after, limit = NUMBER_OF_FEATURED_POSTS, @@ -169,14 +172,14 @@ class RssRepository( } fun posts( - selectedFeedLink: String?, + selectedFeedId: String?, unreadOnly: Boolean? = null, after: Instant = Instant.DISTANT_PAST ): PagingSource { return QueryPagingSource( countQuery = postQueries.count( - feedLink = selectedFeedLink, + sourceId = selectedFeedId, featuredPostsLimit = NUMBER_OF_FEATURED_POSTS, unreadOnly = unreadOnly, postsAfter = after, @@ -185,7 +188,7 @@ class RssRepository( context = ioDispatcher, queryProvider = { limit, offset -> postQueries.posts( - feedLink = selectedFeedLink, + sourceId = selectedFeedId, featuredPostsLimit = NUMBER_OF_FEATURED_POSTS, unreadOnly = unreadOnly, postsAfter = after, @@ -250,16 +253,18 @@ class RssRepository( feedQueries .feeds( mapper = { + id: String, name: String, icon: String, description: String, + link: String, homepageLink: String, createdAt: Instant, - link: String, pinnedAt: Instant?, lastCleanUpAt: Instant?, alwaysFetchSourceArticle: Boolean -> Feed( + id = id, name = name, icon = icon, description = description, @@ -299,24 +304,26 @@ class RssRepository( ) } - suspend fun feed(feedLink: String, postsAfter: Instant = Instant.DISTANT_PAST): Flow { + suspend fun feed(feedId: String, postsAfter: Instant = Instant.DISTANT_PAST): Flow { return withContext(ioDispatcher) { feedQueries .feedWithUnreadPostsCount( - link = feedLink, + id = feedId, postsAfter = postsAfter, mapper = { + id: String, name: String, icon: String, description: String, + link: String, homepageLink: String, createdAt: Instant, - link: String, pinnedAt: Instant?, lastCleanUpAt: Instant?, alwaysFetchSourceArticle: Boolean, numberOfUnreadPosts: Long -> Feed( + id = id, name = name, icon = icon, description = description, @@ -335,24 +342,26 @@ class RssRepository( } } - suspend fun feedBlocking(feedLink: String, postsAfter: Instant = Instant.DISTANT_PAST): Feed { + suspend fun feedBlocking(feedId: String, postsAfter: Instant = Instant.DISTANT_PAST): Feed { return withContext(ioDispatcher) { feedQueries .feedWithUnreadPostsCount( - link = feedLink, + id = feedId, postsAfter = postsAfter, mapper = { + id: String, name: String, icon: String, description: String, + link: String, homepageLink: String, createdAt: Instant, - link: String, pinnedAt: Instant?, lastCleanUpAt: Instant?, alwaysFetchSourceArticle: Boolean, numberOfUnreadPosts: Long -> Feed( + id = id, name = name, icon = icon, description = description, @@ -370,18 +379,18 @@ class RssRepository( } } - suspend fun removeFeed(feedLink: String) { - withContext(ioDispatcher) { feedQueries.remove(feedLink) } + suspend fun removeFeed(feedId: String) { + withContext(ioDispatcher) { feedQueries.remove(feedId) } } suspend fun removeFeeds(feeds: Set) { withContext(ioDispatcher) { - feedQueries.transaction { feeds.forEach { feed -> feedQueries.remove(feed.link) } } + feedQueries.transaction { feeds.forEach { feed -> feedQueries.remove(feed.id) } } } } - suspend fun updateFeedName(newFeedName: String, feedLink: String) { - withContext(ioDispatcher) { feedQueries.updateFeedName(newFeedName, feedLink) } + suspend fun updateFeedName(newFeedName: String, feedId: String) { + withContext(ioDispatcher) { feedQueries.updateFeedName(newFeedName, feedId) } } fun search(searchQuery: String, sortOrder: SearchSortOrder): PagingSource { @@ -414,12 +423,12 @@ class RssRepository( ) } - suspend fun hasPost(link: String): Boolean { - return withContext(ioDispatcher) { postQueries.hasPost(link).executeAsOne() } + suspend fun hasPost(id: String): Boolean { + return withContext(ioDispatcher) { postQueries.hasPost(id).executeAsOne() } } - suspend fun hasFeed(link: String): Boolean { - return withContext(ioDispatcher) { feedQueries.hasFeed(link).executeAsOne() } + suspend fun hasFeed(id: String): Boolean { + return withContext(ioDispatcher) { feedQueries.hasFeed(id).executeAsOne() } } suspend fun toggleFeedPinStatus(feed: Feed) { @@ -429,14 +438,14 @@ class RssRepository( } else { null } - withContext(ioDispatcher) { feedQueries.updatePinnedAt(pinnedAt = now, link = feed.link) } + withContext(ioDispatcher) { feedQueries.updatePinnedAt(pinnedAt = now, id = feed.id) } } suspend fun pinFeeds(feeds: Set) { val now = Clock.System.now() withContext(ioDispatcher) { feedQueries.transaction { - feeds.forEach { feed -> feedQueries.updatePinnedAt(pinnedAt = now, link = feed.link) } + feeds.forEach { feed -> feedQueries.updatePinnedAt(pinnedAt = now, id = feed.id) } } } } @@ -444,15 +453,11 @@ class RssRepository( suspend fun unPinFeeds(feeds: Set) { withContext(ioDispatcher) { feedQueries.transaction { - feeds.forEach { feed -> feedQueries.updatePinnedAt(pinnedAt = null, link = feed.link) } + feeds.forEach { feed -> feedQueries.updatePinnedAt(pinnedAt = null, id = feed.id) } } } } - fun numberOfPinnedFeeds(): Flow { - return feedQueries.numberOfPinnedFeeds().asFlow().mapToOne(ioDispatcher) - } - fun hasFeeds(): Flow { return feedQueries.numberOfFeeds().asFlow().mapToOne(ioDispatcher).map { it > 0 } } @@ -467,27 +472,27 @@ class RssRepository( .distinct() } - suspend fun updateFeedsLastCleanUpAt(feeds: List) { + suspend fun updateFeedsLastCleanUpAt(feedIds: List) { withContext(ioDispatcher) { feedQueries.transaction { - feeds.forEach { feedLink -> - feedQueries.updateLastCleanUpAt(lastCleanUpAt = Clock.System.now(), link = feedLink) + feedIds.forEach { feedId -> + feedQueries.updateLastCleanUpAt(lastCleanUpAt = Clock.System.now(), id = feedId) } } } } - suspend fun markPostsInFeedAsRead(feedLink: String, postsAfter: Instant = Instant.DISTANT_PAST) { - withContext(ioDispatcher) { postQueries.markPostsInFeedAsRead(feedLink, postsAfter) } + suspend fun markPostsInFeedAsRead(feedId: String, postsAfter: Instant = Instant.DISTANT_PAST) { + withContext(ioDispatcher) { postQueries.markPostsInFeedAsRead(feedId, postsAfter) } } suspend fun post(postId: String): Post { return withContext(ioDispatcher) { postQueries.post(postId, ::Post).executeAsOne() } } - suspend fun updateFeedAlwaysFetchSource(feedLink: String, newValue: Boolean) { + suspend fun updateFeedAlwaysFetchSource(feedId: String, newValue: Boolean) { return withContext(ioDispatcher) { - feedQueries.updateAlwaysFetchSourceArticle(newValue, feedLink) + feedQueries.updateAlwaysFetchSourceArticle(newValue, feedId) } } diff --git a/shared/src/commonMain/sqldelight/databases/14.db b/shared/src/commonMain/sqldelight/databases/14.db new file mode 100644 index 0000000000000000000000000000000000000000..7049bf1e8856b73ba921fc3872ea6dafbcab7a42 GIT binary patch literal 86016 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 :postsAfter -GROUP BY f.link +LEFT JOIN post p ON f.id = p.sourceId AND p.date > :postsAfter +GROUP BY f.id ORDER BY CASE WHEN :orderBy = 'latest' THEN f.createdAt END DESC, CASE WHEN :orderBy = 'oldest' THEN f.createdAt END ASC, @@ -59,52 +61,54 @@ LIMIT :limit OFFSET :offset; pinnedFeedsPaginated: SELECT + f.id, f.name, f.icon, f.description, + f.link, f.homepageLink, f.createdAt, - f.link, f.pinnedAt, f.lastCleanUpAt, COUNT(CASE WHEN p.read = 0 THEN 1 ELSE NULL END) AS numberOfUnreadPosts FROM feed f -LEFT JOIN post p ON f.link = p.feedLink AND p.date > :postsAfter +LEFT JOIN post p ON f.id = p.sourceId AND p.date > :postsAfter WHERE pinnedAt IS NOT NULL -GROUP BY f.link +GROUP BY f.id ORDER BY f.pinnedAt DESC LIMIT :limit OFFSET :offset; feed: SELECT * FROM feed -WHERE link = :link +WHERE id = :id ORDER BY pinnedAt DESC, createdAt DESC LIMIT 1; feedWithUnreadPostsCount: SELECT + f.id, f.name, f.icon, f.description, + f.link, f.homepageLink, f.createdAt, - f.link, f.pinnedAt, f.lastCleanUpAt, f.alwaysFetchSourceArticle, COUNT(CASE WHEN p.read = 0 THEN 1 ELSE NULL END) AS numberOfUnreadPosts FROM feed f -LEFT JOIN post p ON f.link = p.feedLink AND p.date > :postsAfter -WHERE f.link = :link +LEFT JOIN post p ON f.id = p.sourceId AND p.date > :postsAfter +WHERE f.id = :id ORDER BY pinnedAt DESC, createdAt DESC LIMIT 1; updateFeedName: -UPDATE feed SET name = :newFeedName WHERE link = :link; +UPDATE feed SET name = :newFeedName WHERE id = :id; hasFeed: -SELECT EXISTS(SELECT 1 FROM feed WHERE link = :link); +SELECT EXISTS(SELECT 1 FROM feed WHERE id = :id); updatePinnedAt: -UPDATE feed SET pinnedAt = :pinnedAt WHERE link = :link; +UPDATE feed SET pinnedAt = :pinnedAt WHERE id = :id; numberOfPinnedFeeds: SELECT COUNT(*) FROM feed WHERE pinnedAt IS NOT NULL; @@ -113,7 +117,7 @@ numberOfFeeds: SELECT COUNT(*) FROM feed; updateLastCleanUpAt: -UPDATE feed SET lastCleanUpAt = :lastCleanUpAt WHERE link = :link; +UPDATE feed SET lastCleanUpAt = :lastCleanUpAt WHERE id = :id; updateAlwaysFetchSourceArticle: -UPDATE feed SET alwaysFetchSourceArticle = :alwaysFetchSourceArticle WHERE link = :link; +UPDATE feed SET alwaysFetchSourceArticle = :alwaysFetchSourceArticle WHERE id = :id; diff --git a/shared/src/commonMain/sqldelight/dev/sasikanth/rss/reader/database/FeedSearchFTS.sq b/shared/src/commonMain/sqldelight/dev/sasikanth/rss/reader/database/FeedSearchFTS.sq index f14db6289..3072990c2 100644 --- a/shared/src/commonMain/sqldelight/dev/sasikanth/rss/reader/database/FeedSearchFTS.sq +++ b/shared/src/commonMain/sqldelight/dev/sasikanth/rss/reader/database/FeedSearchFTS.sq @@ -1,25 +1,26 @@ CREATE VIRTUAL TABLE IF NOT EXISTS feed_search USING FTS5( - name TEXT NOT NULL, - link TEXT NOT NULL PRIMARY KEY UNINDEXED, - tokenize="trigram" + id TEXT NOT NULL PRIMARY KEY, + name TEXT NOT NULL, + link TEXT NOT NULL, + tokenize="trigram" ); CREATE TRIGGER IF NOT EXISTS feed_search_fts_BEFORE_DELETE BEFORE DELETE ON feed -BEGIN DELETE FROM feed_search WHERE link = old.link; +BEGIN DELETE FROM feed_search WHERE id = old.id; END; CREATE TRIGGER IF NOT EXISTS feed_search_fts_AFTER_UPDATE AFTER UPDATE ON feed -BEGIN UPDATE OR IGNORE feed_search SET name = new.name WHERE link = new.link; +BEGIN UPDATE OR IGNORE feed_search SET name = new.name WHERE id = new.id; END; CREATE TRIGGER IF NOT EXISTS feed_search_fts_AFTER_INSERT AFTER INSERT ON feed -BEGIN INSERT OR IGNORE INTO feed_search(name, link) VALUES (new.name, new.link); +BEGIN INSERT OR IGNORE INTO feed_search(id, name, link) VALUES (new.id, new.name, new.link); END; countSearchResults: @@ -27,24 +28,25 @@ SELECT COUNT(*) FROM feed_search WHERE ( :searchQuery = '""' OR - link IN (SELECT link FROM feed_search WHERE feed_search MATCH :searchQuery) + id IN (SELECT id FROM feed_search WHERE feed_search MATCH :searchQuery) ); search: SELECT + f.id, f.name, f.icon, f.description, + f.link, f.homepageLink, f.createdAt, - f.link, f.pinnedAt, f.lastCleanUpAt, COUNT(CASE WHEN p.read = 0 THEN 1 ELSE NULL END) AS numberOfUnreadPosts FROM feed_search fs -INNER JOIN feed f ON f.link = fs.link -LEFT JOIN post p ON f.link = p.feedLink AND p.date > :postsAfter +INNER JOIN feed f ON f.id = fs.id +LEFT JOIN post p ON p.sourceId = f.id AND p.date > :postsAfter WHERE fs.name MATCH :searchQuery -GROUP BY f.link +GROUP BY f.id ORDER BY pinnedAt DESC , createdAt DESC LIMIT :limit OFFSET :offset; diff --git a/shared/src/commonMain/sqldelight/dev/sasikanth/rss/reader/database/Post.sq b/shared/src/commonMain/sqldelight/dev/sasikanth/rss/reader/database/Post.sq index 8bc7bdeac..a14d0b2e5 100644 --- a/shared/src/commonMain/sqldelight/dev/sasikanth/rss/reader/database/Post.sq +++ b/shared/src/commonMain/sqldelight/dev/sasikanth/rss/reader/database/Post.sq @@ -3,24 +3,24 @@ import kotlinx.datetime.Instant; CREATE TABLE post( id TEXT NOT NULL PRIMARY KEY, + sourceId TEXT NOT NULL, title TEXT NOT NULL, description TEXT NOT NULL, + rawContent TEXT, imageUrl TEXT, date INTEGER AS Instant NOT NULL, - feedLink TEXT NOT NULL, link TEXT NOT NULL, - bookmarked INTEGER AS Boolean NOT NULL DEFAULT 0, commentsLink TEXT DEFAULT NULL, + bookmarked INTEGER AS Boolean NOT NULL DEFAULT 0, read INTEGER AS Boolean NOT NULL DEFAULT 0, - rawContent TEXT, - FOREIGN KEY(feedLink) REFERENCES feed(link) ON DELETE CASCADE + FOREIGN KEY(sourceId) REFERENCES feed(id) ON DELETE CASCADE ); -CREATE INDEX post_feed_link_index ON post(feedLink); +CREATE INDEX post_source_id_index ON post(sourceId); upsert: -INSERT INTO post(id, title, description, rawContent, imageUrl, date, feedLink, link, commentsLink) -VALUES (:id, :title, :description, :rawContent, :imageUrl, :date, :feedLink, :link, :commnetsLink) +INSERT INTO post(id, sourceId, title, description, rawContent, imageUrl, date, link, commentsLink) +VALUES (:id, :sourceId, :title, :description, :rawContent, :imageUrl, :date, :link, :commnetsLink) ON CONFLICT(id) DO UPDATE SET title = excluded.title, description = excluded.description, rawContent = excluded.rawContent, imageUrl = excluded.imageUrl, date = excluded.date; @@ -28,13 +28,13 @@ count: SELECT COUNT(*) FROM post WHERE (:unreadOnly IS NULL OR post.read != :unreadOnly) AND - (:feedLink IS NULL OR post.feedLink = :feedLink) AND + (:sourceId IS NULL OR post.sourceId = :sourceId) AND -- Skip featured posts -- post.id NOT IN ( SELECT post.id FROM post WHERE (:unreadOnly IS NULL OR post.read != :unreadOnly) AND - (:feedLink IS NULL OR post.feedLink = :feedLink) AND + (:sourceId IS NULL OR post.sourceId = :sourceId) AND post.imageUrl IS NOT NULL AND post.date > :postsAfter ORDER BY post.date DESC LIMIT :featuredPostsLimit @@ -46,22 +46,22 @@ ORDER BY post.date DESC; featuredPosts: SELECT post.id, + sourceId, post.title, post.description, post.imageUrl, post.date, post.link, + post.commentsLink, post.bookmarked, + post.read, feed.name AS feedName, - feed.icon AS feedIcon, - feedLink, - post.commentsLink, - post.read + feed.icon AS feedIcon FROM post -INNER JOIN feed ON post.feedLink == feed.link +INNER JOIN feed ON post.sourceId == feed.id WHERE (:unreadOnly IS NULL OR post.read != :unreadOnly) AND - (:feedLink IS NULL OR post.feedLink = :feedLink) AND + (:sourceId IS NULL OR post.sourceId = :sourceId) AND post.imageUrl IS NOT NULL AND post.date > :postsAfter ORDER BY post.date DESC LIMIT :limit; @@ -69,28 +69,28 @@ ORDER BY post.date DESC LIMIT :limit; posts: SELECT post.id, + sourceId, post.title, post.description, post.imageUrl, post.date, post.link, + post.commentsLink, post.bookmarked, + post.read, feed.name AS feedName, - feed.icon AS feedIcon, - feedLink, - post.commentsLink, - post.read + feed.icon AS feedIcon FROM post -INNER JOIN feed ON post.feedLink == feed.link +INNER JOIN feed ON post.sourceId == feed.id WHERE (:unreadOnly IS NULL OR post.read != :unreadOnly) AND - (:feedLink IS NULL OR post.feedLink = :feedLink) AND + (:sourceId IS NULL OR post.sourceId = :sourceId) AND -- Skip featured posts -- post.id NOT IN ( SELECT post.id FROM post WHERE (:unreadOnly IS NULL OR post.read != :unreadOnly) AND - (:feedLink IS NULL OR post.feedLink = :feedLink) AND + (:sourceId IS NULL OR post.sourceId = :sourceId) AND post.imageUrl IS NOT NULL AND post.date > :postsAfter ORDER BY post.date DESC LIMIT :featuredPostsLimit @@ -109,12 +109,12 @@ UPDATE post SET read = :read WHERE id = :id; deleteReadPosts: DELETE FROM post WHERE post.read == 1 AND post.date < :before -RETURNING post.feedLink; +RETURNING post.sourceId; markPostsInFeedAsRead: UPDATE post SET read = CASE WHEN read != 1 THEN 1 ELSE read END -WHERE feedLink = :feedLink AND date > :after; +WHERE sourceId = :sourceId AND date > :after; post: SELECT * FROM post diff --git a/shared/src/commonMain/sqldelight/dev/sasikanth/rss/reader/database/PostSearchFTS.sq b/shared/src/commonMain/sqldelight/dev/sasikanth/rss/reader/database/PostSearchFTS.sq index 574bec50e..7bc2ccee3 100644 --- a/shared/src/commonMain/sqldelight/dev/sasikanth/rss/reader/database/PostSearchFTS.sq +++ b/shared/src/commonMain/sqldelight/dev/sasikanth/rss/reader/database/PostSearchFTS.sq @@ -30,20 +30,20 @@ SELECT COUNT(*) FROM post_search WHERE post_search MATCH :searchQuery; search: SELECT post.id, + sourceId, post_search.title, post_search.description, post.imageUrl, post.date, post.link, + post.commentsLink, post.bookmarked, + post.read, feed.name AS feedName, - feed.icon AS feedIcon, - feedLink, - post.commentsLink, - post.read + feed.icon AS feedIcon FROM post_search INNER JOIN post ON post.id == post_search.id -INNER JOIN feed ON post.feedLink == feed.link +INNER JOIN feed ON post.sourceId == feed.id WHERE post_search MATCH :searchQuery ORDER BY CASE WHEN :sortOrder = 'oldest' THEN post.date END ASC, diff --git a/shared/src/commonMain/sqldelight/migrations/13.sqm b/shared/src/commonMain/sqldelight/migrations/13.sqm new file mode 100644 index 000000000..b2fbc1b14 --- /dev/null +++ b/shared/src/commonMain/sqldelight/migrations/13.sqm @@ -0,0 +1,204 @@ +import kotlin.Boolean; +import kotlinx.datetime.Instant; + +DROP INDEX feed_link_index; +DROP INDEX post_feed_link_index; + +DROP TRIGGER post_bookmarked; +DROP TRIGGER post_unbookmarked; +DROP TRIGGER post_content_update; + +DROP TRIGGER post_search_fts_BEFORE_DELETE; +DROP TRIGGER post_search_fts_AFTER_UPDATE; +DROP TRIGGER post_search_fts_AFTER_INSERT; + +DROP TRIGGER feed_search_fts_BEFORE_DELETE; +DROP TRIGGER feed_search_fts_AFTER_UPDATE; +DROP TRIGGER feed_search_fts_AFTER_INSERT; + +-- Start: Migrate feed table to include id column -- +ALTER TABLE feed ADD COLUMN id TEXT NOT NULL DEFAULT ''; +UPDATE feed SET id = link; + +CREATE TABLE feed_temp( + id TEXT NOT NULL PRIMARY KEY, + name TEXT NOT NULL, + icon TEXT NOT NULL, + description TEXT NOT NULL, + link TEXT NOT NULL, + homepageLink TEXT NOT NULL, + createdAt INTEGER AS Instant NOT NULL, + pinnedAt INTEGER AS Instant, + lastCleanUpAt INTEGER AS Instant, + alwaysFetchSourceArticle INTEGER AS Boolean NOT NULL DEFAULT 0 +); + +INSERT INTO feed_temp +SELECT + id, + name, + icon, + description, + link, + homepageLink, + createdAt, + pinnedAt, + lastCleanUpAt, + alwaysFetchSourceArticle +FROM feed; + +DROP TABLE feed; + +ALTER TABLE feed_temp RENAME TO feed; + +CREATE INDEX feed_link_index ON feed(link); +-- End: Migrate feed table to include id column -- + +-- Start: Migrate feed search table to include id column -- +DROP TABLE feed_search; + +CREATE VIRTUAL TABLE IF NOT EXISTS feed_search USING FTS5( + id TEXT NOT NULL PRIMARY KEY, + name TEXT NOT NULL, + link TEXT NOT NULL, + tokenize="trigram" +); + +INSERT INTO feed_search SELECT id, name, link FROM feed; + +CREATE TRIGGER IF NOT EXISTS +feed_search_fts_BEFORE_DELETE +BEFORE DELETE ON feed +BEGIN DELETE FROM feed_search WHERE id = old.id; +END; + +CREATE TRIGGER IF NOT EXISTS +feed_search_fts_AFTER_UPDATE +AFTER UPDATE ON feed +BEGIN UPDATE OR IGNORE feed_search SET name = new.name WHERE id = new.id; +END; + +CREATE TRIGGER IF NOT EXISTS +feed_search_fts_AFTER_INSERT +AFTER INSERT ON feed +BEGIN INSERT OR IGNORE INTO feed_search(id, name, link) VALUES (new.id, new.name, new.link); +END; +-- End: Migrate feed search table to include id column -- + +-- Start: Migrate post table to include source id column -- +CREATE TABLE post_temp( + id TEXT NOT NULL PRIMARY KEY, + sourceId TEXT NOT NULL, + title TEXT NOT NULL, + description TEXT NOT NULL, + rawContent TEXT, + imageUrl TEXT, + date INTEGER AS Instant NOT NULL, + link TEXT NOT NULL, + commentsLink TEXT DEFAULT NULL, + bookmarked INTEGER AS Boolean NOT NULL DEFAULT 0, + read INTEGER AS Boolean NOT NULL DEFAULT 0, + FOREIGN KEY(sourceId) REFERENCES feed(id) ON DELETE CASCADE +); + +INSERT INTO post_temp +SELECT + id, + feedLink, + title, + description, + rawContent, + imageUrl, + date, + link, + commentsLink, + bookmarked, + read +FROM post; + +DROP TABLE post; + +ALTER TABLE post_temp RENAME TO post; + +CREATE INDEX post_source_id_index ON post(sourceId); +-- End: Migrate post table to include source id column -- + +-- Start: Migrate post search table triggers -- +CREATE TRIGGER IF NOT EXISTS +post_search_fts_BEFORE_DELETE +BEFORE DELETE ON post +BEGIN DELETE FROM post_search WHERE id = old.id; +END; + +CREATE TRIGGER IF NOT EXISTS +post_search_fts_AFTER_UPDATE +AFTER UPDATE ON post +BEGIN UPDATE OR IGNORE post_search SET title = new.title, description = new.description WHERE id = new.id; +END; + +CREATE TRIGGER IF NOT EXISTS +post_search_fts_AFTER_INSERT +AFTER INSERT ON post +BEGIN INSERT OR IGNORE INTO post_search(id, title, description, link) VALUES (new.id, new.title, new.description, new.link); +END; +-- End: Migrate post search table triggers -- + +-- Start: Migrate bookmark table to include source id column -- +CREATE TABLE bookmark_temp( + id TEXT NOT NULL PRIMARY KEY, + sourceId TEXT NOT NULL, + title TEXT NOT NULL, + description TEXT NOT NULL, + imageUrl TEXT, + date INTEGER AS Instant NOT NULL, + link TEXT NOT NULL, + commentsLink TEXT DEFAULT NULL, + bookmarked INTEGER AS Boolean NOT NULL DEFAULT 0, + read INTEGER AS Boolean NOT NULL DEFAULT 0, + feedName TEXT NOT NULL, + feedIcon TEXT NOT NULL +); + +INSERT INTO bookmark_temp +SELECT + id, + feedLink, + title, + description, + imageUrl, + date, + link, + commentsLink, + bookmarked, + read, + feedName, + feedIcon +FROM bookmark; + +DROP TABLE bookmark; + +ALTER TABLE bookmark_temp RENAME TO bookmark; + +CREATE TRIGGER IF NOT EXISTS +post_bookmarked +AFTER UPDATE OF bookmarked ON post WHEN new.bookmarked == 1 +BEGIN + INSERT OR REPLACE INTO bookmark(id, sourceId, title, description, imageUrl, date, link, commentsLink, bookmarked, read, feedName, feedIcon) + SELECT new.id, new.sourceId, new.title, new.description, new.imageUrl, new.date, new.link, new.commentsLink, new.bookmarked, new.read, feed.name, feed.icon + FROM feed WHERE feed.id == new.sourceId; +END; + +CREATE TRIGGER IF NOT EXISTS +post_unbookmarked +AFTER UPDATE OF bookmarked ON post WHEN new.bookmarked == 0 +BEGIN DELETE FROM bookmark WHERE id = new.id; +END; + +CREATE TRIGGER IF NOT EXISTS +post_content_update +AFTER UPDATE OF title, description, imageUrl, date, read ON post WHEN new.bookmarked == 1 +BEGIN + UPDATE OR IGNORE bookmark SET title = new.title, description = new.description, imageUrl = new.imageUrl, date = new.date, commentsLink = new.commentsLink, read = new.read + WHERE id = new.id; +END; +-- End: Migrate bookmark table to include id column -- diff --git a/shared/src/commonTest/kotlin/dev/sasikanth/rss/reader/opml/FeedsOpmlTest.kt b/shared/src/commonTest/kotlin/dev/sasikanth/rss/reader/opml/FeedsOpmlTest.kt index c09eb0516..6e5e70517 100644 --- a/shared/src/commonTest/kotlin/dev/sasikanth/rss/reader/opml/FeedsOpmlTest.kt +++ b/shared/src/commonTest/kotlin/dev/sasikanth/rss/reader/opml/FeedsOpmlTest.kt @@ -31,6 +31,7 @@ class FeedsOpmlTest { val feeds = listOf( Feed( + id = "e8d31cec-2893-54d0-bcae-7f134713e532", name = "The Verge", icon = "https://icon.horse/icon/theverge.com", description = "The Verge", @@ -40,6 +41,7 @@ class FeedsOpmlTest { pinnedAt = null ), Feed( + id = "c90003bd-b1e6-5545-ba59-3d2128d658a7", name = "Hacker News", icon = "https://icon.horse/icon/news.ycombinator.com", description = "Hacker News", From 46247ff9a7a3150af88e1537a3fc373ec6ea612f Mon Sep 17 00:00:00 2001 From: Sasikanth Miriyampalli Date: Sat, 13 Apr 2024 17:09:19 +0530 Subject: [PATCH 5/8] Add feed groups table to DB (#442) * Add db table for feed groups * Fetch 4 feed icons when loading feed groups These will be used for group preview in feeds bottom sheet * Add count query for feed_group table * Convert groups query to paginated query * Rename `feed_group` table to `feedGroup` * Provide feed group queries via DI * Add feed group queries in RSS repository --- .../rss/reader/core/model/local/FeedGroup.kt | 28 +++++++++ .../reader/database/ListToStringAdapter.kt | 30 +++++++++ .../sasikanth/rss/reader/di/DataComponent.kt | 10 +++ .../rss/reader/repository/RssRepository.kt | 58 ++++++++++++++++++ .../src/commonMain/sqldelight/databases/15.db | Bin 0 -> 94208 bytes .../rss/reader/database/FeedGroup.sq | 42 +++++++++++++ .../commonMain/sqldelight/migrations/14.sqm | 11 ++++ 7 files changed, 179 insertions(+) create mode 100644 core/model/src/commonMain/kotlin/dev/sasikanth/rss/reader/core/model/local/FeedGroup.kt create mode 100644 shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/database/ListToStringAdapter.kt create mode 100644 shared/src/commonMain/sqldelight/databases/15.db create mode 100644 shared/src/commonMain/sqldelight/dev/sasikanth/rss/reader/database/FeedGroup.sq create mode 100644 shared/src/commonMain/sqldelight/migrations/14.sqm diff --git a/core/model/src/commonMain/kotlin/dev/sasikanth/rss/reader/core/model/local/FeedGroup.kt b/core/model/src/commonMain/kotlin/dev/sasikanth/rss/reader/core/model/local/FeedGroup.kt new file mode 100644 index 000000000..9597f8630 --- /dev/null +++ b/core/model/src/commonMain/kotlin/dev/sasikanth/rss/reader/core/model/local/FeedGroup.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2024 Sasikanth Miriyampalli + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.sasikanth.rss.reader.core.model.local + +import kotlinx.datetime.Instant + +data class FeedGroup( + val id: String, + val name: String, + val feedIds: Set, + val feedIcons: Set, + val createdAt: Instant, + val updatedAt: Instant, +) diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/database/ListToStringAdapter.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/database/ListToStringAdapter.kt new file mode 100644 index 000000000..683d44aa6 --- /dev/null +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/database/ListToStringAdapter.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2024 Sasikanth Miriyampalli + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.sasikanth.rss.reader.database + +import app.cash.sqldelight.ColumnAdapter + +internal object ListToStringAdapter : ColumnAdapter, String> { + + override fun decode(databaseValue: String): List { + return databaseValue.split(", ") + } + + override fun encode(value: List): String { + return value.joinToString() + } +} diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/di/DataComponent.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/di/DataComponent.kt index f6693f05c..7b4edd3e8 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/di/DataComponent.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/di/DataComponent.kt @@ -20,6 +20,8 @@ import app.cash.sqldelight.db.SqlDriver import dev.sasikanth.rss.reader.database.Bookmark import dev.sasikanth.rss.reader.database.DateAdapter import dev.sasikanth.rss.reader.database.Feed +import dev.sasikanth.rss.reader.database.FeedGroup +import dev.sasikanth.rss.reader.database.ListToStringAdapter import dev.sasikanth.rss.reader.database.Post import dev.sasikanth.rss.reader.database.ReaderDatabase import dev.sasikanth.rss.reader.database.migrations.SQLCodeMigrations @@ -45,6 +47,12 @@ internal interface DataComponent : SqlDriverPlatformComponent, DataStorePlatform lastCleanUpAtAdapter = DateAdapter ), bookmarkAdapter = Bookmark.Adapter(dateAdapter = DateAdapter), + feedGroupAdapter = + FeedGroup.Adapter( + feedIdsAdapter = ListToStringAdapter, + createdAtAdapter = DateAdapter, + updatedAtAdapter = DateAdapter + ) ) } @@ -61,4 +69,6 @@ internal interface DataComponent : SqlDriverPlatformComponent, DataStorePlatform @Provides fun providesFeedSearchFTSQueries(database: ReaderDatabase) = database.feedSearchFTSQueries + + @Provides fun providesFeedGroupQueries(database: ReaderDatabase) = database.feedGroupQueries } diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/repository/RssRepository.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/repository/RssRepository.kt index 1f0d2046f..f94082b49 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/repository/RssRepository.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/repository/RssRepository.kt @@ -20,12 +20,15 @@ import app.cash.sqldelight.coroutines.asFlow import app.cash.sqldelight.coroutines.mapToList import app.cash.sqldelight.coroutines.mapToOne import app.cash.sqldelight.paging3.QueryPagingSource +import com.benasher44.uuid.uuid4 import dev.sasikanth.rss.reader.core.model.local.Feed +import dev.sasikanth.rss.reader.core.model.local.FeedGroup import dev.sasikanth.rss.reader.core.model.local.Post import dev.sasikanth.rss.reader.core.model.local.PostWithMetadata import dev.sasikanth.rss.reader.core.network.fetcher.FeedFetchResult import dev.sasikanth.rss.reader.core.network.fetcher.FeedFetcher import dev.sasikanth.rss.reader.database.BookmarkQueries +import dev.sasikanth.rss.reader.database.FeedGroupQueries import dev.sasikanth.rss.reader.database.FeedQueries import dev.sasikanth.rss.reader.database.FeedSearchFTSQueries import dev.sasikanth.rss.reader.database.PostQueries @@ -52,6 +55,7 @@ class RssRepository( private val postSearchFTSQueries: PostSearchFTSQueries, private val bookmarkQueries: BookmarkQueries, private val feedSearchFTSQueries: FeedSearchFTSQueries, + private val feedGroupQueries: FeedGroupQueries, dispatchersProvider: DispatchersProvider ) { @@ -496,6 +500,60 @@ class RssRepository( } } + fun feedGroups(): PagingSource { + return QueryPagingSource( + countQuery = feedGroupQueries.count(), + transacter = feedGroupQueries, + context = ioDispatcher, + queryProvider = { limit, offset -> + feedGroupQueries.groups( + limit = limit, + offset = offset, + mapper = { + id: String, + name: String, + feedIds: List, + feedIcons: String, + createdAt: Instant, + updatedAt: Instant -> + FeedGroup( + id = id, + name = name, + feedIds = feedIds.toSet(), + feedIcons = feedIcons.split(",").toSet(), + createdAt = createdAt, + updatedAt = updatedAt + ) + } + ) + } + ) + } + + suspend fun createGroup(name: String) { + withContext(ioDispatcher) { + feedGroupQueries.createGroup( + id = uuid4().toString(), + name = name, + feedIds = emptyList(), + createdAt = Clock.System.now(), + updatedAt = Clock.System.now() + ) + } + } + + suspend fun updateGroupName(groupId: String, name: String) { + withContext(ioDispatcher) { feedGroupQueries.updateGroupName(name, groupId) } + } + + suspend fun updateFeedIds(groupId: String, feedIds: Set) { + withContext(ioDispatcher) { feedGroupQueries.updateFeedIds(feedIds.toList(), groupId) } + } + + suspend fun deleteGroup(groupId: String) { + withContext(ioDispatcher) { feedGroupQueries.deleteGroup(groupId) } + } + private fun sanitizeSearchQuery(searchQuery: String): String { return searchQuery.replace(Regex.fromLiteral("\""), "\"\"").run { "\"$this\"" } } diff --git a/shared/src/commonMain/sqldelight/databases/15.db b/shared/src/commonMain/sqldelight/databases/15.db new file mode 100644 index 0000000000000000000000000000000000000000..2e54cf7794188fa2dfe0ca40218236380975f796 GIT binary patch literal 94208 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 literal 0 HcmV?d00001 diff --git a/shared/src/commonMain/sqldelight/dev/sasikanth/rss/reader/database/FeedGroup.sq b/shared/src/commonMain/sqldelight/dev/sasikanth/rss/reader/database/FeedGroup.sq new file mode 100644 index 000000000..9c203efb3 --- /dev/null +++ b/shared/src/commonMain/sqldelight/dev/sasikanth/rss/reader/database/FeedGroup.sq @@ -0,0 +1,42 @@ +import kotlin.String; +import kotlin.collections.List; +import kotlinx.datetime.Instant; + +CREATE TABLE IF NOT EXISTS feedGroup( + id TEXT NOT NULL PRIMARY KEY, + name TEXT NOT NULL, + feedIds TEXT AS List NOT NULL, + createdAt INTEGER AS Instant NOT NULL, + updatedAt INTEGER AS Instant NOT NULL +); + +count: +SELECT COUNT(*) FROM feedGroup; + +groups: +SELECT + id, + name, + feedIds, + (SELECT GROUP_CONCAT(feed.icon) + FROM feed + WHERE feed.id IN (feedGroup.feedIds) + LIMIT 4) AS feedIcons, + createdAt, + updatedAt +FROM feedGroup +LIMIT :limit OFFSET :offset; + +createGroup: +INSERT INTO feedGroup(id, name, feedIds, createdAt, updatedAt) +VALUES (:id, :name, :feedIds, :createdAt, :updatedAt) +ON CONFLICT(name) DO NOTHING; + +updateGroupName: +UPDATE feedGroup SET name = :name WHERE id = :id; + +updateFeedIds: +UPDATE feedGroup SET feedIds = :feedIds WHERE id = :id; + +deleteGroup: +DELETE FROM feedGroup WHERE id = :id; diff --git a/shared/src/commonMain/sqldelight/migrations/14.sqm b/shared/src/commonMain/sqldelight/migrations/14.sqm new file mode 100644 index 000000000..4809f8990 --- /dev/null +++ b/shared/src/commonMain/sqldelight/migrations/14.sqm @@ -0,0 +1,11 @@ +import kotlin.String; +import kotlin.collections.List; +import kotlinx.datetime.Instant; + +CREATE TABLE IF NOT EXISTS feedGroup( + id TEXT NOT NULL PRIMARY KEY, + name TEXT NOT NULL, + feedIds TEXT AS List NOT NULL, + createdAt INTEGER AS Instant NOT NULL, + updatedAt INTEGER AS Instant NOT NULL +); From 5a365941ad9589adf8576032e297e3507efc43f2 Mon Sep 17 00:00:00 2001 From: Sasikanth Miriyampalli Date: Sat, 13 Apr 2024 18:05:25 +0530 Subject: [PATCH 6/8] Remove on conflict for `createGroup` query --- .../sqldelight/dev/sasikanth/rss/reader/database/FeedGroup.sq | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/shared/src/commonMain/sqldelight/dev/sasikanth/rss/reader/database/FeedGroup.sq b/shared/src/commonMain/sqldelight/dev/sasikanth/rss/reader/database/FeedGroup.sq index 9c203efb3..4e9cf6a1c 100644 --- a/shared/src/commonMain/sqldelight/dev/sasikanth/rss/reader/database/FeedGroup.sq +++ b/shared/src/commonMain/sqldelight/dev/sasikanth/rss/reader/database/FeedGroup.sq @@ -29,8 +29,7 @@ LIMIT :limit OFFSET :offset; createGroup: INSERT INTO feedGroup(id, name, feedIds, createdAt, updatedAt) -VALUES (:id, :name, :feedIds, :createdAt, :updatedAt) -ON CONFLICT(name) DO NOTHING; +VALUES (:id, :name, :feedIds, :createdAt, :updatedAt); updateGroupName: UPDATE feedGroup SET name = :name WHERE id = :id; From 15f0b2147aac975ac68547f3bef80143960ef350 Mon Sep 17 00:00:00 2001 From: Sasikanth Miriyampalli Date: Sat, 13 Apr 2024 18:05:47 +0530 Subject: [PATCH 7/8] Handle no feed ids when fetching feed icons in `groups` query --- .../dev/sasikanth/rss/reader/database/FeedGroup.sq | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/shared/src/commonMain/sqldelight/dev/sasikanth/rss/reader/database/FeedGroup.sq b/shared/src/commonMain/sqldelight/dev/sasikanth/rss/reader/database/FeedGroup.sq index 4e9cf6a1c..c4f608380 100644 --- a/shared/src/commonMain/sqldelight/dev/sasikanth/rss/reader/database/FeedGroup.sq +++ b/shared/src/commonMain/sqldelight/dev/sasikanth/rss/reader/database/FeedGroup.sq @@ -18,10 +18,10 @@ SELECT id, name, feedIds, - (SELECT GROUP_CONCAT(feed.icon) - FROM feed - WHERE feed.id IN (feedGroup.feedIds) - LIMIT 4) AS feedIcons, + COALESCE((SELECT GROUP_CONCAT(feed.icon) + FROM feed + WHERE feed.id IN (feedGroup.feedIds) + LIMIT 4), '') AS feedIcons, createdAt, updatedAt FROM feedGroup From d36c3eb910410a0ad741fa7cb992aafe112b9ab4 Mon Sep 17 00:00:00 2001 From: Sasikanth Miriyampalli Date: Sat, 13 Apr 2024 18:16:24 +0530 Subject: [PATCH 8/8] Create common interface named `Source` for `Feed` and `FeedGroup` --- .../rss/reader/core/model/local/Feed.kt | 5 ++-- .../rss/reader/core/model/local/FeedGroup.kt | 5 ++-- .../rss/reader/core/model/local/Source.kt | 27 +++++++++++++++++++ 3 files changed, 33 insertions(+), 4 deletions(-) create mode 100644 core/model/src/commonMain/kotlin/dev/sasikanth/rss/reader/core/model/local/Source.kt diff --git a/core/model/src/commonMain/kotlin/dev/sasikanth/rss/reader/core/model/local/Feed.kt b/core/model/src/commonMain/kotlin/dev/sasikanth/rss/reader/core/model/local/Feed.kt index 7012feb94..70d96222d 100644 --- a/core/model/src/commonMain/kotlin/dev/sasikanth/rss/reader/core/model/local/Feed.kt +++ b/core/model/src/commonMain/kotlin/dev/sasikanth/rss/reader/core/model/local/Feed.kt @@ -20,7 +20,7 @@ import kotlinx.datetime.Instant @Immutable data class Feed( - val id: String, + override val id: String, val name: String, val icon: String, val description: String, @@ -31,4 +31,5 @@ data class Feed( val lastCleanUpAt: Instant? = null, val numberOfUnreadPosts: Long = 0L, val alwaysFetchSourceArticle: Boolean = false, -) + override val sourceType: SourceType = SourceType.Feed +) : Source diff --git a/core/model/src/commonMain/kotlin/dev/sasikanth/rss/reader/core/model/local/FeedGroup.kt b/core/model/src/commonMain/kotlin/dev/sasikanth/rss/reader/core/model/local/FeedGroup.kt index 9597f8630..965e917bf 100644 --- a/core/model/src/commonMain/kotlin/dev/sasikanth/rss/reader/core/model/local/FeedGroup.kt +++ b/core/model/src/commonMain/kotlin/dev/sasikanth/rss/reader/core/model/local/FeedGroup.kt @@ -19,10 +19,11 @@ package dev.sasikanth.rss.reader.core.model.local import kotlinx.datetime.Instant data class FeedGroup( - val id: String, + override val id: String, val name: String, val feedIds: Set, val feedIcons: Set, val createdAt: Instant, val updatedAt: Instant, -) + override val sourceType: SourceType = SourceType.FeedGroup +) : Source diff --git a/core/model/src/commonMain/kotlin/dev/sasikanth/rss/reader/core/model/local/Source.kt b/core/model/src/commonMain/kotlin/dev/sasikanth/rss/reader/core/model/local/Source.kt new file mode 100644 index 000000000..d0ee3ccd2 --- /dev/null +++ b/core/model/src/commonMain/kotlin/dev/sasikanth/rss/reader/core/model/local/Source.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2024 Sasikanth Miriyampalli + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.sasikanth.rss.reader.core.model.local + +interface Source { + val id: String + val sourceType: SourceType +} + +enum class SourceType { + Feed, + FeedGroup +}