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..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,14 +20,16 @@ import kotlinx.datetime.Instant @Immutable data class Feed( + override 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, 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 new file mode 100644 index 000000000..965e917bf --- /dev/null +++ b/core/model/src/commonMain/kotlin/dev/sasikanth/rss/reader/core/model/local/FeedGroup.kt @@ -0,0 +1,29 @@ +/* + * 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( + 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/Post.kt b/core/model/src/commonMain/kotlin/dev/sasikanth/rss/reader/core/model/local/Post.kt index 590a5bb8b..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 @@ -19,14 +19,15 @@ package dev.sasikanth.rss.reader.core.model.local 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 70c5d9501..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 @@ -20,15 +20,16 @@ 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/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 +} 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..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 @@ -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 @@ -115,7 +116,7 @@ class AppPresenter( is ModalConfig.FeedInfo -> { Modals.FeedInfo( presenter = - feedPresenter(modalConfig.feedLink, componentContext) { modalNavigation.dismiss() } + feedPresenter(modalConfig.feedId, componentContext) { modalNavigation.dismiss() } ) } } @@ -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,11 +215,11 @@ class AppPresenter( @Serializable data object About : Config - @Serializable data class Reader(val postLink: String) : Config + @Serializable data class Reader(val postId: String) : Config } @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/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..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 @@ -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 hasFeed = rssRepository.hasFeed(post.feedLink) + val hasPost = rssRepository.hasPost(post.id) + val hasFeed = rssRepository.hasFeed(post.sourceId) if (hasPost && hasFeed) { - openReaderView(post.link) + openReaderView(post) } else { openLink(post.link) } @@ -140,10 +137,10 @@ class BookmarksPresenter( private fun onPostBookmarkClicked(post: PostWithMetadata) { coroutineScope.launch { - if (rssRepository.hasFeed(post.feedLink)) { - rssRepository.updateBookmarkStatus(bookmarked = !post.bookmarked, link = post.link) + if (rssRepository.hasFeed(post.sourceId)) { + 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/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/database/migrations/SQLCodeMigrations.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/database/migrations/SQLCodeMigrations.kt new file mode 100644 index 000000000..724d523c6 --- /dev/null +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/database/migrations/SQLCodeMigrations.kt @@ -0,0 +1,135 @@ +/* + * 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(), afterVersion13()) + } + + private fun afterVersion13(): AfterVersion { + return AfterVersion(13) { driver -> + val feedIds = FeedsIdsQuery(driver).executeAsList() + feedIds.forEach { feedId -> migrateFeedsLinkIdsToUuid(feedId, driver) } + } + } + + private fun afterVersion12(): AfterVersion { + return AfterVersion(12) { driver -> + val ids = PostsIdsQuery(driver).executeAsList() + ids.forEach { id -> migratePostLinkIdsToUuid(driver, id) } + } + } + + 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() + + 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 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)!! }) { + 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..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 @@ -15,12 +15,16 @@ */ 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.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 import dev.sasikanth.rss.reader.di.scopes.AppScope import me.tatarka.inject.annotations.Provides @@ -43,9 +47,17 @@ internal interface DataComponent : SqlDriverPlatformComponent, DataStorePlatform lastCleanUpAtAdapter = DateAdapter ), bookmarkAdapter = Bookmark.Adapter(dateAdapter = DateAdapter), + feedGroupAdapter = + FeedGroup.Adapter( + feedIdsAdapter = ListToStringAdapter, + createdAtAdapter = DateAdapter, + updatedAtAdapter = DateAdapter + ) ) } + @Provides @AppScope fun providesMigrations(): Array = SQLCodeMigrations.migrations() + @Provides fun providesFeedQueries(database: ReaderDatabase) = database.feedQueries @Provides fun providesPostQueries(database: ReaderDatabase) = database.postQueries @@ -57,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/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 59df2e1e0..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 @@ -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..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 @@ -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 } @@ -190,32 +190,30 @@ 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.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) { 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) } } private fun onPostBookmarkClicked(post: PostWithMetadata) { coroutineScope.launch { - rssRepository.updateBookmarkStatus(bookmarked = !post.bookmarked, link = post.link) + rssRepository.updateBookmarkStatus(bookmarked = !post.bookmarked, id = post.id) } } @@ -256,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 ) @@ -266,7 +264,7 @@ class HomePresenter( rssRepository .featuredPosts( - selectedFeedLink = selectedFeed?.link, + selectedFeedId = selectedFeed?.id, unreadOnly = unreadOnly, after = postsAfter ) @@ -388,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 13241d6b7..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,8 +180,8 @@ internal fun FeaturedSection( onClick = { onItemClick(featuredPost) }, onBookmarkClick = { onPostBookmarkClick(featuredPost) }, onCommentsClick = { onPostCommentsClick(featuredPost.commentsLink!!) }, - onSourceClick = { onPostSourceClick(featuredPost.feedLink) }, - onTogglePostReadClick = { onTogglePostReadClick(featuredPost.link, featuredPost.read) } + 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 cebadd9b4..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,12 +166,12 @@ 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 = { 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..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,8 +113,8 @@ internal fun PostsList( onClick = { onPostClicked(post) }, onPostBookmarkClick = { onPostBookmarkClick(post) }, onPostCommentsClick = { onPostCommentsClick(post.commentsLink!!) }, - onPostSourceClick = { onPostSourceClick(post.feedLink) }, - togglePostReadClick = { onTogglePostReadClick(post.link, post.read) } + onPostSourceClick = { onPostSourceClick(post.sourceId) }, + 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..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 @@ -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,48 +90,48 @@ 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 feed = rssRepository.feedBlocking(post.feedLink) + val post = rssRepository.post(postId) + val feed = rssRepository.feedBlocking(post.sourceId) _state.update { it.copy( @@ -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..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 @@ -33,6 +36,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 @@ -51,6 +55,7 @@ class RssRepository( private val postSearchFTSQueries: PostSearchFTSQueries, private val bookmarkQueries: BookmarkQueries, private val feedSearchFTSQueries: FeedSearchFTSQueries, + private val feedGroupQueries: FeedGroupQueries, dispatchersProvider: DispatchersProvider ) { @@ -72,13 +77,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 { @@ -89,13 +97,14 @@ class RssRepository( feedPayload.posts.forEach { post -> 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 ) } @@ -140,9 +149,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) } @@ -150,13 +159,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, @@ -167,14 +176,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, @@ -183,7 +192,7 @@ class RssRepository( context = ioDispatcher, queryProvider = { limit, offset -> postQueries.posts( - feedLink = selectedFeedLink, + sourceId = selectedFeedId, featuredPostsLimit = NUMBER_OF_FEATURED_POSTS, unreadOnly = unreadOnly, postsAfter = after, @@ -195,16 +204,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( @@ -248,16 +257,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, @@ -297,24 +308,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, @@ -333,24 +346,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, @@ -368,18 +383,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 { @@ -412,12 +427,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) { @@ -427,14 +442,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) } } } } @@ -442,15 +457,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 } } @@ -465,32 +476,82 @@ 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(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) { + suspend fun updateFeedAlwaysFetchSource(feedId: String, newValue: Boolean) { return withContext(ioDispatcher) { - feedQueries.updateAlwaysFetchSourceArticle(newValue, feedLink) + feedQueries.updateAlwaysFetchSourceArticle(newValue, feedId) } } - suspend fun feedsCount(): Long { - return withContext(ioDispatcher) { feedQueries.count().executeAsOne() } + 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 { 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 000000000..b29cc7098 Binary files /dev/null and b/shared/src/commonMain/sqldelight/databases/13.db differ diff --git a/shared/src/commonMain/sqldelight/databases/14.db b/shared/src/commonMain/sqldelight/databases/14.db new file mode 100644 index 000000000..7049bf1e8 Binary files /dev/null and b/shared/src/commonMain/sqldelight/databases/14.db differ diff --git a/shared/src/commonMain/sqldelight/databases/15.db b/shared/src/commonMain/sqldelight/databases/15.db new file mode 100644 index 000000000..2e54cf779 Binary files /dev/null and b/shared/src/commonMain/sqldelight/databases/15.db differ diff --git a/shared/src/commonMain/sqldelight/dev/sasikanth/rss/reader/database/Bookmark.sq b/shared/src/commonMain/sqldelight/dev/sasikanth/rss/reader/database/Bookmark.sq index 44b3ddba3..0f4587af0 100644 --- a/shared/src/commonMain/sqldelight/dev/sasikanth/rss/reader/database/Bookmark.sq +++ b/shared/src/commonMain/sqldelight/dev/sasikanth/rss/reader/database/Bookmark.sq @@ -2,32 +2,33 @@ import kotlin.Boolean; import kotlinx.datetime.Instant; CREATE TABLE bookmark ( + 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 PRIMARY KEY, + 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, - feedLink TEXT NOT NULL, - commentsLink TEXT DEFAULT NULL, - read INTEGER AS Boolean NOT NULL DEFAULT 0 + feedIcon TEXT NOT NULL ); CREATE TRIGGER IF NOT EXISTS post_bookmarked AFTER UPDATE OF bookmarked ON post WHEN new.bookmarked == 1 BEGIN - INSERT OR REPLACE INTO bookmark(title, description, imageUrl, date, link, bookmarked, commentsLink, feedName, feedIcon, feedLink, read) - SELECT 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; + 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 link = new.link; +BEGIN DELETE FROM bookmark WHERE id = new.id; END; CREATE TRIGGER IF NOT EXISTS @@ -35,14 +36,9 @@ 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 link = new.link; + WHERE id = new.id; END; -/** -TODO: Create trigger for updating post read status when bookmark read status is updated -When created we didn't had UI for that feature -*/ - countBookmarks: SELECT COUNT(*) FROM bookmark; @@ -51,4 +47,4 @@ SELECT * FROM bookmark ORDER BY date DESC LIMIT :limit OFFSET :offset; deleteBookmark: -DELETE FROM bookmark WHERE link = :link; +DELETE FROM bookmark WHERE id = :id; diff --git a/shared/src/commonMain/sqldelight/dev/sasikanth/rss/reader/database/Feed.sq b/shared/src/commonMain/sqldelight/dev/sasikanth/rss/reader/database/Feed.sq index 0accbde2e..19f751ad1 100644 --- a/shared/src/commonMain/sqldelight/dev/sasikanth/rss/reader/database/Feed.sq +++ b/shared/src/commonMain/sqldelight/dev/sasikanth/rss/reader/database/Feed.sq @@ -2,12 +2,13 @@ import kotlin.Boolean; import kotlinx.datetime.Instant; CREATE TABLE feed( + 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, - link TEXT NOT NULL PRIMARY KEY, pinnedAt INTEGER AS Instant, lastCleanUpAt INTEGER AS Instant, alwaysFetchSourceArticle INTEGER AS Boolean NOT NULL DEFAULT 0 @@ -16,14 +17,14 @@ CREATE TABLE feed( CREATE INDEX feed_link_index ON feed(link); upsert: -INSERT INTO feed(name, icon, description, homepageLink, createdAt, link) -VALUES (?, ?, ?, ?, ?, ?) -ON CONFLICT(link) DO +INSERT INTO feed(id, name, icon, description, homepageLink, createdAt, link) +VALUES (?, ?, ?, ?, ?, ?, ?) +ON CONFLICT(id) DO UPDATE SET icon = excluded.icon, description = excluded.description, homepageLink = excluded.homepageLink; remove: -DELETE FROM feed WHERE link = :link; +DELETE FROM feed WHERE id = :id; count: SELECT COUNT(*) FROM feed; @@ -37,18 +38,19 @@ ORDER BY pinnedAt DESC, createdAt DESC; feedsPaginated: 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 -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/FeedGroup.sq b/shared/src/commonMain/sqldelight/dev/sasikanth/rss/reader/database/FeedGroup.sq new file mode 100644 index 000000000..c4f608380 --- /dev/null +++ b/shared/src/commonMain/sqldelight/dev/sasikanth/rss/reader/database/FeedGroup.sq @@ -0,0 +1,41 @@ +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, + COALESCE((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); + +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/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 0a367c0f3..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 @@ -2,38 +2,39 @@ import kotlin.Boolean; 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 PRIMARY KEY, - bookmarked INTEGER AS Boolean NOT NULL DEFAULT 0, + link TEXT NOT NULL, 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(title, description, rawContent, imageUrl, date, feedLink, link, commentsLink) -VALUES (:title, :description, :rawContent, :imageUrl, :date, :feedLink, :link, :commnetsLink) -ON CONFLICT(link) DO +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; 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.link NOT IN ( - SELECT post.link FROM post + 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 @@ -44,50 +45,52 @@ 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; 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.link NOT IN ( - SELECT post.link FROM post + 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 @@ -98,24 +101,24 @@ ORDER BY post.date DESC LIMIT :limit OFFSET :offset; updateBookmarkStatus: -UPDATE post SET bookmarked = :bookmarked WHERE link = :link; +UPDATE post SET bookmarked = :bookmarked WHERE id = :id; updateReadStatus: -UPDATE post SET read = :read WHERE link = :link; +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 -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); 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..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 @@ -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,20 +29,21 @@ 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.link == post_search.link -INNER JOIN feed ON post.feedLink == feed.link +INNER JOIN post ON post.id == post_search.id +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/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/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/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 +); 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", 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), ) ) }