From a33ba48ed10fe7e32d7422654a11339c73de73f1 Mon Sep 17 00:00:00 2001 From: Sasikanth Miriyampalli Date: Sat, 27 Jan 2024 08:26:03 +0530 Subject: [PATCH 1/3] Fix app crashing when opening bookmarked posts (#252) * Add query to check if post exists in main posts table * Open link when bookmarked post does exists in main post table or feed doesn't exists --- .../sasikanth/rss/reader/app/AppPresenter.kt | 2 +- .../rss/reader/bookmarks/BookmarksEffect.kt | 2 ++ .../reader/bookmarks/BookmarksPresenter.kt | 30 +++++++++++++++++-- .../reader/bookmarks/ui/BookmarksScreen.kt | 11 +++++++ .../rss/reader/repository/RssRepository.kt | 4 +++ .../dev/sasikanth/rss/reader/database/Post.sq | 3 ++ 6 files changed, 49 insertions(+), 3 deletions(-) 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 4ca72fe22..43973d32e 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 @@ -63,7 +63,7 @@ private typealias BookmarkPresenterFactory = ( ComponentContext, goBack: () -> Unit, - openPost: (String) -> Unit, + openReaderView: (String) -> Unit, ) -> BookmarksPresenter private typealias SettingsPresenterFactory = diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/bookmarks/BookmarksEffect.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/bookmarks/BookmarksEffect.kt index 513eb351c..d5d2abf18 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/bookmarks/BookmarksEffect.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/bookmarks/BookmarksEffect.kt @@ -17,3 +17,5 @@ package dev.sasikanth.rss.reader.bookmarks sealed interface BookmarksEffect + +data class OpenLink(val link: String) : BookmarksEffect 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 d8ff1da33..bfb316e92 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 @@ -45,7 +45,7 @@ class BookmarksPresenter( private val rssRepository: RssRepository, @Assisted componentContext: ComponentContext, @Assisted private val goBack: () -> Unit, - @Assisted private val openPost: (postLink: String) -> Unit, + @Assisted private val openReaderView: (postLink: String) -> Unit, ) : ComponentContext by componentContext { private val presenterInstance = @@ -63,7 +63,12 @@ class BookmarksPresenter( fun dispatch(event: BookmarksEvent) { when (event) { BookmarksEvent.BackClicked -> goBack() - is BookmarksEvent.OnPostClicked -> openPost(event.post.link) + is BookmarksEvent.OnPostClicked -> + presenterInstance.onPostClicked( + post = event.post, + openReaderView = { openReaderView(it) }, + openLink = { presenterInstance.openLink(it) } + ) else -> { // no-op } @@ -101,6 +106,23 @@ class BookmarksPresenter( } } + fun onPostClicked( + post: PostWithMetadata, + openReaderView: (postLink: String) -> Unit, + openLink: (postLink: String) -> Unit + ) { + coroutineScope.launch { + val hasPost = rssRepository.hasPost(post.link) + val hasFeed = rssRepository.hasFeed(post.feedLink) + + if (hasPost && hasFeed) { + openReaderView(post.link) + } else { + openLink(post.link) + } + } + } + private fun onPostBookmarkClicked(post: PostWithMetadata) { coroutineScope.launch { if (rssRepository.hasFeed(post.feedLink)) { @@ -123,5 +145,9 @@ class BookmarksPresenter( override fun onDestroy() { coroutineScope.cancel() } + + fun openLink(link: String) { + coroutineScope.launch { effects.emit(OpenLink(link)) } + } } } 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 40aeb6238..7c5213798 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 @@ -38,6 +38,7 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue @@ -51,6 +52,7 @@ import androidx.compose.ui.unit.dp import app.cash.paging.compose.collectAsLazyPagingItems import dev.sasikanth.rss.reader.bookmarks.BookmarksEvent import dev.sasikanth.rss.reader.bookmarks.BookmarksPresenter +import dev.sasikanth.rss.reader.bookmarks.OpenLink import dev.sasikanth.rss.reader.components.CompactFloatingActionButton import dev.sasikanth.rss.reader.home.ui.PostListItem import dev.sasikanth.rss.reader.platform.LocalLinkHandler @@ -58,6 +60,7 @@ import dev.sasikanth.rss.reader.resources.icons.Bookmarks import dev.sasikanth.rss.reader.resources.icons.TwineIcons import dev.sasikanth.rss.reader.resources.strings.LocalStrings import dev.sasikanth.rss.reader.ui.AppTheme +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch @Composable @@ -73,6 +76,14 @@ internal fun BookmarksScreen( val layoutDirection = LocalLayoutDirection.current val linkHandler = LocalLinkHandler.current + LaunchedEffect(Unit) { + bookmarksPresenter.effects.collectLatest { effect -> + when (effect) { + is OpenLink -> coroutineScope.launch { linkHandler.openLink(effect.link) } + } + } + } + Scaffold( modifier = modifier, topBar = { 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 0278e5b33..9aabb8f85 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 @@ -292,6 +292,10 @@ class RssRepository( ) } + suspend fun hasPost(link: String): Boolean { + return withContext(ioDispatcher) { postQueries.hasPost(link).executeAsOne() } + } + suspend fun hasFeed(link: String): Boolean { return withContext(ioDispatcher) { feedQueries.hasFeed(link).executeAsOne() } } 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 fc823b20a..4cabc5ee2 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 @@ -103,3 +103,6 @@ WHERE feedLink = :feedLink; post: SELECT * FROM post WHERE post.link = :link; + +hasPost: +SELECT EXISTS(SELECT 1 FROM post WHERE link = :link); From afbf2e25dc91ebf843755321d7ae30550fa2859d Mon Sep 17 00:00:00 2001 From: Sasikanth Miriyampalli Date: Sat, 27 Jan 2024 09:09:47 +0530 Subject: [PATCH 2/3] Reduce alpha of read items in home screen and search screen --- .../dev/sasikanth/rss/reader/home/ui/FeaturedPostItem.kt | 8 +++++++- .../kotlin/dev/sasikanth/rss/reader/home/ui/PostList.kt | 6 +++++- .../dev/sasikanth/rss/reader/search/ui/SearchScreen.kt | 1 + 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/ui/FeaturedPostItem.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/ui/FeaturedPostItem.kt index 20197f453..65c9f8d09 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/ui/FeaturedPostItem.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/ui/FeaturedPostItem.kt @@ -37,6 +37,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.layout.ContentScale @@ -71,7 +72,12 @@ internal fun FeaturedPostItem( ) { val isLargeScreenLayout = LocalWindowSizeClass.current.widthSizeClass == WindowWidthSizeClass.Expanded - Box(modifier = Modifier.clip(MaterialTheme.shapes.extraLarge).clickable(onClick = onClick)) { + Box( + modifier = + Modifier.clip(MaterialTheme.shapes.extraLarge) + .clickable(onClick = onClick) + .alpha(if (item.read) 0.65f else 1f) + ) { if (isLargeScreenLayout) { LargeScreenFeaturedPostItem( item = item, 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 3dced23f5..7398d8e1e 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 @@ -40,6 +40,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale @@ -106,6 +107,7 @@ internal fun PostsList( PostListItem( item = post, enablePostSource = true, + reduceReadItemAlpha = true, onClick = { onPostClicked(post) }, onPostBookmarkClick = { onPostBookmarkClick(post) }, onPostCommentsClick = { onPostCommentsClick(post.commentsLink!!) }, @@ -130,13 +132,15 @@ fun PostListItem( onClick: () -> Unit, onPostBookmarkClick: () -> Unit, onPostCommentsClick: () -> Unit, - onPostSourceClick: () -> Unit + onPostSourceClick: () -> Unit, + reduceReadItemAlpha: Boolean = false ) { Column( modifier = Modifier.clickable(onClick = onClick) .windowInsetsPadding(WindowInsets.systemBars.only(WindowInsetsSides.Horizontal)) .padding(postListPadding) + .alpha(if (item.read && reduceReadItemAlpha) 0.65f else 1f) ) { Row( modifier = Modifier.padding(start = 24.dp, top = 20.dp, end = 24.dp), 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 52eae5552..df7087a90 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 @@ -136,6 +136,7 @@ internal fun SearchScreen(searchPresenter: SearchPresenter, modifier: Modifier = PostListItem( item = post, enablePostSource = false, + reduceReadItemAlpha = true, onClick = { searchPresenter.dispatch(SearchEvent.OnPostClicked(post)) }, onPostBookmarkClick = { searchPresenter.dispatch(SearchEvent.OnPostBookmarkClick(post)) From fc36f8a4a93b838d896bb5e53db2cde9174aa27e Mon Sep 17 00:00:00 2001 From: Sasikanth Miriyampalli Date: Sat, 27 Jan 2024 09:35:31 +0530 Subject: [PATCH 3/3] Add tags table in the database (#253) * Add UUID dependency * Add `Tag` table to the app * Add local tag model * Add required tag queries --- core/model/build.gradle.kts | 1 + .../rss/reader/core/model/local/Tag.kt | 27 ++++++++ gradle/libs.versions.toml | 2 + shared/build.gradle.kts | 1 + .../rss/reader/database/UuidAdapter.kt | 32 ++++++++++ .../sasikanth/rss/reader/di/DataComponent.kt | 10 +++ .../rss/reader/repository/TagRepository.kt | 61 +++++++++++++++++++ .../dev/sasikanth/rss/reader/database/Tag.sq | 22 +++++++ 8 files changed, 156 insertions(+) create mode 100644 core/model/src/commonMain/kotlin/dev/sasikanth/rss/reader/core/model/local/Tag.kt create mode 100644 shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/database/UuidAdapter.kt create mode 100644 shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/repository/TagRepository.kt create mode 100644 shared/src/commonMain/sqldelight/dev/sasikanth/rss/reader/database/Tag.sq diff --git a/core/model/build.gradle.kts b/core/model/build.gradle.kts index a95b1fa6d..e5820a949 100644 --- a/core/model/build.gradle.kts +++ b/core/model/build.gradle.kts @@ -25,6 +25,7 @@ kotlin { implementation(libs.kotlinx.datetime) // Require this for `@Immutable` annotation for models implementation(libs.compose.runtime) + implementation(libs.uuid) } } } diff --git a/core/model/src/commonMain/kotlin/dev/sasikanth/rss/reader/core/model/local/Tag.kt b/core/model/src/commonMain/kotlin/dev/sasikanth/rss/reader/core/model/local/Tag.kt new file mode 100644 index 000000000..e50005a0f --- /dev/null +++ b/core/model/src/commonMain/kotlin/dev/sasikanth/rss/reader/core/model/local/Tag.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 + +import com.benasher44.uuid.Uuid +import kotlinx.datetime.Instant + +data class Tag( + val id: Uuid, + val label: String, + val createdAt: Instant, + val updatedAt: Instant, +) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a3f0396cf..018895512 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -47,6 +47,7 @@ xmlutil = "0.86.3" ktxml = "0.2.3" uri = "0.0.16" webview = "1.8.4" +uuid = "0.8.2" [libraries] compose_runtime = { module = "org.jetbrains.compose.runtime:runtime", version.ref = "compose" } @@ -108,6 +109,7 @@ xmlutil-serialization = { module = "io.github.pdvrieze.xmlutil:serialization", v ktxml = { module = "org.kobjects.ktxml:core", version.ref = "ktxml" } uri = { module = "com.eygraber:uri-kmp", version.ref = "uri" } webview = { module = "io.github.kevinnzou:compose-webview-multiplatform", version.ref = "webview" } +uuid = { module = "com.benasher44:uuid", version.ref = "uuid" } [plugins] android_application = { id = "com.android.application", version.ref = "android_gradle_plugin" } diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index 9880c9506..2cbaf646a 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -120,6 +120,7 @@ kotlin { implementation(libs.stately.iso.collections) implementation(libs.bundles.xmlutil) api(libs.webview) + implementation(libs.uuid) } commonTest.dependencies { implementation(libs.kotlin.test) diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/database/UuidAdapter.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/database/UuidAdapter.kt new file mode 100644 index 000000000..ecd3519c8 --- /dev/null +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/database/UuidAdapter.kt @@ -0,0 +1,32 @@ +/* + * 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 +import com.benasher44.uuid.Uuid +import com.benasher44.uuid.uuidFrom + +internal object UuidAdapter : ColumnAdapter { + + override fun decode(databaseValue: String): Uuid { + return uuidFrom(databaseValue) + } + + override fun encode(value: Uuid): String { + return value.toString() + } +} 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..fdeee3415 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 @@ -21,6 +21,8 @@ import dev.sasikanth.rss.reader.database.DateAdapter import dev.sasikanth.rss.reader.database.Feed import dev.sasikanth.rss.reader.database.Post import dev.sasikanth.rss.reader.database.ReaderDatabase +import dev.sasikanth.rss.reader.database.Tag +import dev.sasikanth.rss.reader.database.UuidAdapter import dev.sasikanth.rss.reader.di.scopes.AppScope import me.tatarka.inject.annotations.Provides @@ -43,6 +45,12 @@ internal interface DataComponent : SqlDriverPlatformComponent, DataStorePlatform lastCleanUpAtAdapter = DateAdapter ), bookmarkAdapter = Bookmark.Adapter(dateAdapter = DateAdapter), + tagAdapter = + Tag.Adapter( + idAdapter = UuidAdapter, + createdAtAdapter = DateAdapter, + updatedAtAdapter = DateAdapter + ) ) } @@ -57,4 +65,6 @@ internal interface DataComponent : SqlDriverPlatformComponent, DataStorePlatform @Provides fun providesFeedSearchFTSQueries(database: ReaderDatabase) = database.feedSearchFTSQueries + + @Provides fun providesTagQueries(database: ReaderDatabase) = database.tagQueries } diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/repository/TagRepository.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/repository/TagRepository.kt new file mode 100644 index 000000000..78ea9143e --- /dev/null +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/repository/TagRepository.kt @@ -0,0 +1,61 @@ +/* + * 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.repository + +import app.cash.sqldelight.coroutines.asFlow +import app.cash.sqldelight.coroutines.mapToList +import com.benasher44.uuid.Uuid +import com.benasher44.uuid.uuid4 +import dev.sasikanth.rss.reader.core.model.local.Tag +import dev.sasikanth.rss.reader.database.TagQueries +import dev.sasikanth.rss.reader.di.scopes.AppScope +import dev.sasikanth.rss.reader.util.DispatchersProvider +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.withContext +import kotlinx.datetime.Clock +import me.tatarka.inject.annotations.Inject + +@Inject +@AppScope +class TagRepository( + private val dispatchersProvider: DispatchersProvider, + private val tagQueries: TagQueries, +) { + + suspend fun createTag(label: String) { + withContext(dispatchersProvider.io) { + val currentInstant = Clock.System.now() + + tagQueries.saveTag( + id = uuid4(), + label = label, + createdAt = currentInstant, + updatedAt = currentInstant + ) + } + } + + suspend fun deleteTag(id: Uuid) = withContext(dispatchersProvider.io) { tagQueries.deleteTag(id) } + + suspend fun updatedTag(label: String, id: Uuid) { + withContext(dispatchersProvider.io) { tagQueries.updateTag(label = label, id = id) } + } + + fun tags(label: String? = null): Flow> { + return tagQueries.tags(label.orEmpty(), ::Tag).asFlow().mapToList(dispatchersProvider.io) + } +} diff --git a/shared/src/commonMain/sqldelight/dev/sasikanth/rss/reader/database/Tag.sq b/shared/src/commonMain/sqldelight/dev/sasikanth/rss/reader/database/Tag.sq new file mode 100644 index 000000000..a8608fd64 --- /dev/null +++ b/shared/src/commonMain/sqldelight/dev/sasikanth/rss/reader/database/Tag.sq @@ -0,0 +1,22 @@ +import com.benasher44.uuid.Uuid; +import kotlinx.datetime.Instant; + +CREATE TABLE tag ( + id TEXT AS Uuid NOT NULL PRIMARY KEY, + label TEXT NOT NULL, + createdAt INTEGER AS Instant NOT NULL, + updatedAt INTEGER AS Instant NOT NULL +); + +tags: +SELECT * FROM tag WHERE label LIKE :label OR label IS NULL OR label = ''; + +updateTag: +UPDATE tag SET label = :label WHERE id = :id; + +deleteTag: +DELETE FROM tag WHERE id = :id; + +saveTag: +INSERT OR IGNORE INTO tag(id, label, createdAt, updatedAt) +VALUES (:id, :label, :createdAt, :updatedAt);