diff --git a/app/schemas/com.jerboa.db.AppDB/23.json b/app/schemas/com.jerboa.db.AppDB/23.json new file mode 100644 index 000000000..0a5ad6e1f --- /dev/null +++ b/app/schemas/com.jerboa.db.AppDB/23.json @@ -0,0 +1,232 @@ +{ + "formatVersion": 1, + "database": { + "version": 23, + "identityHash": "cb557db7205cb6ae333d14fbcd156c30", + "entities": [ + { + "tableName": "Account", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `current` INTEGER NOT NULL, `instance` TEXT NOT NULL, `name` TEXT NOT NULL, `jwt` TEXT NOT NULL, `default_listing_type` INTEGER NOT NULL DEFAULT 0, `default_sort_type` INTEGER NOT NULL DEFAULT 0, `verification_state` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "current", + "columnName": "current", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "jwt", + "columnName": "jwt", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "defaultListingType", + "columnName": "default_listing_type", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "defaultSortType", + "columnName": "default_sort_type", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "verificationState", + "columnName": "verification_state", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AppSettings", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `font_size` INTEGER NOT NULL DEFAULT 16, `theme` INTEGER NOT NULL DEFAULT 0, `theme_color` INTEGER NOT NULL DEFAULT 0, `viewed_changelog` INTEGER NOT NULL DEFAULT 0, `post_view_mode` INTEGER NOT NULL DEFAULT 0, `show_bottom_nav` INTEGER NOT NULL DEFAULT 1, `show_collapsed_comment_content` INTEGER NOT NULL DEFAULT 0, `show_comment_action_bar_by_default` INTEGER NOT NULL DEFAULT 1, `show_voting_arrows_in_list_view` INTEGER NOT NULL DEFAULT 1, `show_parent_comment_navigation_buttons` INTEGER NOT NULL DEFAULT 0, `navigate_parent_comments_with_volume_buttons` INTEGER NOT NULL DEFAULT 0, `use_custom_tabs` INTEGER NOT NULL DEFAULT 1, `use_private_tabs` INTEGER NOT NULL DEFAULT 0, `secure_window` INTEGER NOT NULL DEFAULT 0, `blur_nsfw` INTEGER NOT NULL DEFAULT 1, `show_text_descriptions_in_navbar` INTEGER NOT NULL DEFAULT 1, `markAsReadOnScroll` INTEGER NOT NULL DEFAULT 0, `backConfirmationMode` INTEGER NOT NULL DEFAULT 1, `show_post_link_previews` INTEGER NOT NULL DEFAULT 1)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fontSize", + "columnName": "font_size", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "16" + }, + { + "fieldPath": "theme", + "columnName": "theme", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "themeColor", + "columnName": "theme_color", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "viewedChangelog", + "columnName": "viewed_changelog", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "postViewMode", + "columnName": "post_view_mode", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "showBottomNav", + "columnName": "show_bottom_nav", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "showCollapsedCommentContent", + "columnName": "show_collapsed_comment_content", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "showCommentActionBarByDefault", + "columnName": "show_comment_action_bar_by_default", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "showVotingArrowsInListView", + "columnName": "show_voting_arrows_in_list_view", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "showParentCommentNavigationButtons", + "columnName": "show_parent_comment_navigation_buttons", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "navigateParentCommentsWithVolumeButtons", + "columnName": "navigate_parent_comments_with_volume_buttons", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "useCustomTabs", + "columnName": "use_custom_tabs", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "usePrivateTabs", + "columnName": "use_private_tabs", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "secureWindow", + "columnName": "secure_window", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "blurNSFW", + "columnName": "blur_nsfw", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "showTextDescriptionsInNavbar", + "columnName": "show_text_descriptions_in_navbar", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "markAsReadOnScroll", + "columnName": "markAsReadOnScroll", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "backConfirmationMode", + "columnName": "backConfirmationMode", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "showPostLinkPreviews", + "columnName": "show_post_link_previews", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'cb557db7205cb6ae333d14fbcd156c30')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/com/jerboa/MainActivity.kt b/app/src/main/java/com/jerboa/MainActivity.kt index 6818132be..dc07e5bed 100644 --- a/app/src/main/java/com/jerboa/MainActivity.kt +++ b/app/src/main/java/com/jerboa/MainActivity.kt @@ -271,6 +271,7 @@ class MainActivity : AppCompatActivity() { usePrivateTabs = appSettings.usePrivateTabs, blurNSFW = appSettings.blurNSFW, showPostLinkPreviews = appSettings.showPostLinkPreviews, + markAsReadOnScroll = appSettings.markAsReadOnScroll, ) } @@ -311,6 +312,7 @@ class MainActivity : AppCompatActivity() { usePrivateTabs = appSettings.usePrivateTabs, blurNSFW = appSettings.blurNSFW, showPostLinkPreviews = appSettings.showPostLinkPreviews, + markAsReadOnScroll = appSettings.markAsReadOnScroll, ) } @@ -382,6 +384,7 @@ class MainActivity : AppCompatActivity() { showPostLinkPreviews = appSettings.showPostLinkPreviews, drawerState = drawerState, onBack = appState::popBackStack, + markAsReadOnScroll = appSettings.markAsReadOnScroll, ) } @@ -415,6 +418,7 @@ class MainActivity : AppCompatActivity() { blurNSFW = appSettings.blurNSFW, showPostLinkPreviews = appSettings.showPostLinkPreviews, drawerState = drawerState, + markAsReadOnScroll = appSettings.markAsReadOnScroll, ) } diff --git a/app/src/main/java/com/jerboa/api/Http.kt b/app/src/main/java/com/jerboa/api/Http.kt index 85bf47658..e1acc18e3 100644 --- a/app/src/main/java/com/jerboa/api/Http.kt +++ b/app/src/main/java/com/jerboa/api/Http.kt @@ -59,6 +59,12 @@ interface API { @POST("post/like") suspend fun likePost(@Body form: CreatePostLike): Response + /** + * Mark post as read. + */ + @POST("post/mark_as_read") + suspend fun markAsRead(@Body form: MarkPostAsRead): Response + /** * Like / vote on a comment. */ diff --git a/app/src/main/java/com/jerboa/db/AppDB.kt b/app/src/main/java/com/jerboa/db/AppDB.kt index b95c6efca..1f23eabb6 100644 --- a/app/src/main/java/com/jerboa/db/AppDB.kt +++ b/app/src/main/java/com/jerboa/db/AppDB.kt @@ -34,11 +34,12 @@ val APP_SETTINGS_DEFAULT = AppSettings( blurNSFW = true, showTextDescriptionsInNavbar = true, backConfirmationMode = 1, + markAsReadOnScroll = false, showPostLinkPreviews = true, ) @Database( - version = 22, + version = 23, entities = [Account::class, AppSettings::class], exportSchema = true, ) diff --git a/app/src/main/java/com/jerboa/db/AppDBMigrations.kt b/app/src/main/java/com/jerboa/db/AppDBMigrations.kt index ccf4396b9..2af65d5c2 100644 --- a/app/src/main/java/com/jerboa/db/AppDBMigrations.kt +++ b/app/src/main/java/com/jerboa/db/AppDBMigrations.kt @@ -287,6 +287,15 @@ val MIGRATION_22_21 = object : Migration(22, 21) { } } +val MIGRATION_22_23 = object : Migration(22, 23) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL(UPDATE_APP_CHANGELOG_UNVIEWED) + database.execSQL( + "ALTER TABLE AppSettings ADD COLUMN markAsReadOnScroll INTEGER NOT NULL DEFAULT 0", + ) + } +} + // Don't forget to test your migration with `./gradlew app:connectAndroidTest` val MIGRATIONS_LIST = arrayOf( MIGRATION_1_2, @@ -311,4 +320,5 @@ val MIGRATIONS_LIST = arrayOf( MIGRATION_20_21, MIGRATION_21_22, MIGRATION_22_21, + MIGRATION_22_23, ) diff --git a/app/src/main/java/com/jerboa/db/entity/AppSettings.kt b/app/src/main/java/com/jerboa/db/entity/AppSettings.kt index 31481b3b0..6df88c736 100644 --- a/app/src/main/java/com/jerboa/db/entity/AppSettings.kt +++ b/app/src/main/java/com/jerboa/db/entity/AppSettings.kt @@ -88,6 +88,11 @@ data class AppSettings( defaultValue = "1", ) val showTextDescriptionsInNavbar: Boolean, + @ColumnInfo( + name = "markAsReadOnScroll", + defaultValue = "0", + ) + val markAsReadOnScroll: Boolean, @ColumnInfo( name = "backConfirmationMode", defaultValue = "1", diff --git a/app/src/main/java/com/jerboa/model/CommunityViewModel.kt b/app/src/main/java/com/jerboa/model/CommunityViewModel.kt index bf017eda8..d02b8965c 100644 --- a/app/src/main/java/com/jerboa/model/CommunityViewModel.kt +++ b/app/src/main/java/com/jerboa/model/CommunityViewModel.kt @@ -7,6 +7,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.jerboa.JerboaAppState import com.jerboa.api.API import com.jerboa.api.ApiState import com.jerboa.api.apiWrapper @@ -23,6 +24,7 @@ import com.jerboa.datatypes.types.GetCommunity import com.jerboa.datatypes.types.GetCommunityResponse import com.jerboa.datatypes.types.GetPosts import com.jerboa.datatypes.types.GetPostsResponse +import com.jerboa.datatypes.types.MarkPostAsRead import com.jerboa.datatypes.types.PostResponse import com.jerboa.datatypes.types.PostView import com.jerboa.datatypes.types.SavePost @@ -53,6 +55,7 @@ class CommunityViewModel : ViewModel(), Initializable { private var blockCommunityRes: ApiState by mutableStateOf(ApiState.Empty) private var blockPersonRes: ApiState by mutableStateOf(ApiState.Empty) + private var markPostRes: ApiState by mutableStateOf(ApiState.Empty) var sortType by mutableStateOf(SortType.Active) private set @@ -118,8 +121,16 @@ class CommunityViewModel : ViewModel(), Initializable { if (newRes.data.posts.isEmpty()) { // Hit the end of the posts prevPage() } - ApiState.Success(GetPostsResponse(mergePosts(oldRes.data.posts, newRes.data.posts))) + ApiState.Success( + GetPostsResponse( + mergePosts( + oldRes.data.posts, + newRes.data.posts, + ), + ), + ) } + else -> { prevPage() oldRes @@ -247,4 +258,22 @@ class CommunityViewModel : ViewModel(), Initializable { else -> {} } } + + fun markPostAsRead( + form: MarkPostAsRead, + appState: JerboaAppState, + ) { + appState.coroutineScope.launch { + markPostRes = ApiState.Loading + markPostRes = apiWrapper(API.getInstance().markAsRead(form)) + + when (val markRes = markPostRes) { + is ApiState.Success -> { + updatePost(markRes.data.post_view) + } + + else -> {} + } + } + } } diff --git a/app/src/main/java/com/jerboa/model/HomeViewModel.kt b/app/src/main/java/com/jerboa/model/HomeViewModel.kt index 04535fc70..f05391000 100644 --- a/app/src/main/java/com/jerboa/model/HomeViewModel.kt +++ b/app/src/main/java/com/jerboa/model/HomeViewModel.kt @@ -8,6 +8,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.jerboa.JerboaAppState import com.jerboa.api.API import com.jerboa.api.ApiState import com.jerboa.api.apiWrapper @@ -17,9 +18,11 @@ import com.jerboa.datatypes.types.BlockPerson import com.jerboa.datatypes.types.BlockPersonResponse import com.jerboa.datatypes.types.CreatePostLike import com.jerboa.datatypes.types.DeletePost +import com.jerboa.datatypes.types.GetPostResponse import com.jerboa.datatypes.types.GetPosts import com.jerboa.datatypes.types.GetPostsResponse import com.jerboa.datatypes.types.ListingType +import com.jerboa.datatypes.types.MarkPostAsRead import com.jerboa.datatypes.types.PostResponse import com.jerboa.datatypes.types.PostView import com.jerboa.datatypes.types.SavePost @@ -39,11 +42,14 @@ class HomeViewModel : ViewModel(), Initializable { var postsRes: ApiState by mutableStateOf(ApiState.Empty) private set + private var postRes: ApiState by mutableStateOf(ApiState.Empty) + private var likePostRes: ApiState by mutableStateOf(ApiState.Empty) private var savePostRes: ApiState by mutableStateOf(ApiState.Empty) private var deletePostRes: ApiState by mutableStateOf(ApiState.Empty) private var blockCommunityRes: ApiState by mutableStateOf(ApiState.Empty) private var blockPersonRes: ApiState by mutableStateOf(ApiState.Empty) + private var markPostRes: ApiState by mutableStateOf(ApiState.Empty) val lazyListState = LazyListState() @@ -100,8 +106,16 @@ class HomeViewModel : ViewModel(), Initializable { if (newRes.data.posts.isEmpty()) { // Hit the end of the posts prevPage() } - ApiState.Success(GetPostsResponse(mergePosts(oldRes.data.posts, newRes.data.posts))) + ApiState.Success( + GetPostsResponse( + mergePosts( + oldRes.data.posts, + newRes.data.posts, + ), + ), + ) } + else -> { prevPage() oldRes @@ -172,7 +186,9 @@ class HomeViewModel : ViewModel(), Initializable { fun updateFromAccount(account: Account) { updateSortType(SortType.values().getOrElse(account.defaultSortType) { sortType }) - updateListingType(ListingType.values().getOrElse(account.defaultListingType) { listingType }) + updateListingType( + ListingType.values().getOrElse(account.defaultListingType) { listingType }, + ) } fun updatePost(postView: PostView) { @@ -182,6 +198,7 @@ class HomeViewModel : ViewModel(), Initializable { val newRes = ApiState.Success(existing.data.copy(posts = newPosts)) postsRes = newRes } + else -> {} } } @@ -219,4 +236,22 @@ class HomeViewModel : ViewModel(), Initializable { auth = jwt, ) } + + fun markPostAsRead( + form: MarkPostAsRead, + appState: JerboaAppState, + ) { + appState.coroutineScope.launch { + markPostRes = ApiState.Loading + markPostRes = apiWrapper(API.getInstance().markAsRead(form)) + + when (val markRes = markPostRes) { + is ApiState.Success -> { + updatePost(markRes.data.post_view) + } + + else -> {} + } + } + } } diff --git a/app/src/main/java/com/jerboa/model/PersonProfileViewModel.kt b/app/src/main/java/com/jerboa/model/PersonProfileViewModel.kt index 301708128..0a5f7b1f4 100644 --- a/app/src/main/java/com/jerboa/model/PersonProfileViewModel.kt +++ b/app/src/main/java/com/jerboa/model/PersonProfileViewModel.kt @@ -7,6 +7,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.jerboa.JerboaAppState import com.jerboa.api.API import com.jerboa.api.ApiState import com.jerboa.api.apiWrapper @@ -22,6 +23,8 @@ import com.jerboa.datatypes.types.DeleteComment import com.jerboa.datatypes.types.DeletePost import com.jerboa.datatypes.types.GetPersonDetails import com.jerboa.datatypes.types.GetPersonDetailsResponse +import com.jerboa.datatypes.types.GetPostResponse +import com.jerboa.datatypes.types.MarkPostAsRead import com.jerboa.datatypes.types.PersonId import com.jerboa.datatypes.types.PostResponse import com.jerboa.datatypes.types.PostView @@ -44,6 +47,8 @@ class PersonProfileViewModel : ViewModel(), Initializable { ) private set + private var postRes: ApiState by mutableStateOf(ApiState.Empty) + private var likePostRes: ApiState by mutableStateOf(ApiState.Empty) private var savePostRes: ApiState by mutableStateOf(ApiState.Empty) private var deletePostRes: ApiState by mutableStateOf(ApiState.Empty) @@ -55,6 +60,8 @@ class PersonProfileViewModel : ViewModel(), Initializable { private var saveCommentRes: ApiState by mutableStateOf(ApiState.Empty) private var deleteCommentRes: ApiState by mutableStateOf(ApiState.Empty) + private var markPostRes: ApiState by mutableStateOf(ApiState.Empty) + var sortType by mutableStateOf(SortType.New) private set var page by mutableIntStateOf(1) @@ -131,6 +138,7 @@ class PersonProfileViewModel : ViewModel(), Initializable { ), ) } + else -> { prevPage() oldRes @@ -287,4 +295,22 @@ class PersonProfileViewModel : ViewModel(), Initializable { else -> {} } } + + fun markPostAsRead( + form: MarkPostAsRead, + appState: JerboaAppState, + ) { + appState.coroutineScope.launch { + markPostRes = ApiState.Loading + markPostRes = apiWrapper(API.getInstance().markAsRead(form)) + + when (val markRes = markPostRes) { + is ApiState.Success -> { + updatePost(markRes.data.post_view) + } + + else -> {} + } + } + } } diff --git a/app/src/main/java/com/jerboa/model/PostViewModel.kt b/app/src/main/java/com/jerboa/model/PostViewModel.kt index de787fe31..6fa5834c5 100644 --- a/app/src/main/java/com/jerboa/model/PostViewModel.kt +++ b/app/src/main/java/com/jerboa/model/PostViewModel.kt @@ -68,6 +68,7 @@ class PostViewModel : ViewModel(), Initializable { private var deletePostRes: ApiState by mutableStateOf(ApiState.Empty) private var blockCommunityRes: ApiState by mutableStateOf(ApiState.Empty) private var blockPersonRes: ApiState by mutableStateOf(ApiState.Empty) + private var markPostRes: ApiState by mutableStateOf(ApiState.Empty) val unExpandedComments = mutableStateListOf() val commentsWithToggledActionBar = mutableStateListOf() @@ -161,6 +162,7 @@ class PostViewModel : ViewModel(), Initializable { commentsRes = ApiState.Success(existing.data.copy(comments = appended)) } + else -> {} } } diff --git a/app/src/main/java/com/jerboa/ui/components/community/CommunityActivity.kt b/app/src/main/java/com/jerboa/ui/components/community/CommunityActivity.kt index d6db47102..f4dfa4c92 100644 --- a/app/src/main/java/com/jerboa/ui/components/community/CommunityActivity.kt +++ b/app/src/main/java/com/jerboa/ui/components/community/CommunityActivity.kt @@ -45,6 +45,7 @@ import com.jerboa.datatypes.types.FollowCommunity import com.jerboa.datatypes.types.GetCommunity import com.jerboa.datatypes.types.GetPosts import com.jerboa.datatypes.types.GetSite +import com.jerboa.datatypes.types.MarkPostAsRead import com.jerboa.datatypes.types.PostView import com.jerboa.datatypes.types.SavePost import com.jerboa.datatypes.types.SortType @@ -68,6 +69,7 @@ import com.jerboa.ui.components.common.getPostViewMode import com.jerboa.ui.components.common.isLoading import com.jerboa.ui.components.common.isRefreshing import com.jerboa.ui.components.post.PostListings +import com.jerboa.ui.components.post.PostViewReturn import com.jerboa.ui.components.post.edit.PostEditReturn import com.jerboa.util.InitializeRoute import com.jerboa.util.doIfReadyElseDisplayInfo @@ -87,6 +89,7 @@ fun CommunityActivity( usePrivateTabs: Boolean, blurNSFW: Boolean, showPostLinkPreviews: Boolean, + markAsReadOnScroll: Boolean, ) { Log.d("jerboa", "got to community activity") val transferCreatePostDepsViaRoot = appState.rootChannel() @@ -103,6 +106,10 @@ fun CommunityActivity( if (communityViewModel.initialized) communityViewModel.updatePost(pv) } + appState.ConsumeReturn(PostViewReturn.POST_VIEW) { pv -> + if (communityViewModel.initialized) communityViewModel.updatePost(pv) + } + InitializeRoute(communityViewModel) { val communityId = communityArg.fold({ it }, { null }) val communityName = communityArg.fold({ null }, { it }) @@ -456,6 +463,19 @@ fun CommunityActivity( showPostLinkPreviews = showPostLinkPreviews, openImageViewer = appState::toView, openLink = appState::openLink, + markAsReadOnScroll = markAsReadOnScroll, + onMarkAsRead = { postView -> + if (!account.isAnon() && !postView.read) { + communityViewModel.markPostAsRead( + MarkPostAsRead( + post_id = postView.post.id, + read = true, + auth = account.jwt, + ), + appState, + ) + } + }, showIfRead = true, showScores = siteViewModel.showScores(), ) diff --git a/app/src/main/java/com/jerboa/ui/components/home/BottomNavActivity.kt b/app/src/main/java/com/jerboa/ui/components/home/BottomNavActivity.kt index f204c6699..35bb1bcae 100644 --- a/app/src/main/java/com/jerboa/ui/components/home/BottomNavActivity.kt +++ b/app/src/main/java/com/jerboa/ui/components/home/BottomNavActivity.kt @@ -65,11 +65,36 @@ enum class NavTab( val iconFilled: ImageVector, val contentDescriptionId: Int, ) { - Home(R.string.bottomBar_label_home, Icons.Outlined.Home, Icons.Filled.Home, R.string.bottomBar_home), - Search(R.string.bottomBar_label_search, Icons.Outlined.Search, Icons.Filled.Search, R.string.bottomBar_search), - Inbox(R.string.bottomBar_label_inbox, Icons.Outlined.Email, Icons.Filled.Email, R.string.bottomBar_inbox), - Saved(R.string.bottomBar_label_bookmarks, Icons.Outlined.Bookmarks, Icons.Filled.Bookmarks, R.string.bottomBar_bookmarks), - Profile(R.string.bottomBar_label_profile, Icons.Outlined.Person, Icons.Filled.Person, R.string.bottomBar_profile), + Home( + R.string.bottomBar_label_home, + Icons.Outlined.Home, + Icons.Filled.Home, + R.string.bottomBar_home, + ), + Search( + R.string.bottomBar_label_search, + Icons.Outlined.Search, + Icons.Filled.Search, + R.string.bottomBar_search, + ), + Inbox( + R.string.bottomBar_label_inbox, + Icons.Outlined.Email, + Icons.Filled.Email, + R.string.bottomBar_inbox, + ), + Saved( + R.string.bottomBar_label_bookmarks, + Icons.Outlined.Bookmarks, + Icons.Filled.Bookmarks, + R.string.bottomBar_bookmarks, + ), + Profile( + R.string.bottomBar_label_profile, + Icons.Outlined.Person, + Icons.Filled.Person, + R.string.bottomBar_profile, + ), ; fun needsLogin() = this == Inbox || this == Saved || this == Profile @@ -201,6 +226,7 @@ fun BottomNavActivity( drawerState = drawerState, blurNSFW = appSettings.blurNSFW, showPostLinkPreviews = appSettings.showPostLinkPreviews, + markAsReadOnScroll = appSettings.markAsReadOnScroll, ) } @@ -239,6 +265,7 @@ fun BottomNavActivity( blurNSFW = appSettings.blurNSFW, showPostLinkPreviews = appSettings.showPostLinkPreviews, drawerState = drawerState, + markAsReadOnScroll = appSettings.markAsReadOnScroll, ) } @@ -256,6 +283,7 @@ fun BottomNavActivity( blurNSFW = appSettings.blurNSFW, showPostLinkPreviews = appSettings.showPostLinkPreviews, drawerState = drawerState, + markAsReadOnScroll = appSettings.markAsReadOnScroll, ) } } diff --git a/app/src/main/java/com/jerboa/ui/components/home/HomeActivity.kt b/app/src/main/java/com/jerboa/ui/components/home/HomeActivity.kt index 96de09b41..70fba2f1d 100644 --- a/app/src/main/java/com/jerboa/ui/components/home/HomeActivity.kt +++ b/app/src/main/java/com/jerboa/ui/components/home/HomeActivity.kt @@ -47,6 +47,7 @@ import com.jerboa.datatypes.types.BlockCommunity import com.jerboa.datatypes.types.BlockPerson import com.jerboa.datatypes.types.CreatePostLike import com.jerboa.datatypes.types.DeletePost +import com.jerboa.datatypes.types.MarkPostAsRead import com.jerboa.datatypes.types.PostView import com.jerboa.datatypes.types.SavePost import com.jerboa.datatypes.types.Tagline @@ -71,6 +72,7 @@ import com.jerboa.ui.components.common.getPostViewMode import com.jerboa.ui.components.common.isLoading import com.jerboa.ui.components.common.isRefreshing import com.jerboa.ui.components.post.PostListings +import com.jerboa.ui.components.post.PostViewReturn import com.jerboa.ui.components.post.edit.PostEditReturn import com.jerboa.util.doIfReadyElseDisplayInfo import kotlinx.collections.immutable.persistentListOf @@ -91,6 +93,7 @@ fun HomeActivity( drawerState: DrawerState, blurNSFW: Boolean, showPostLinkPreviews: Boolean, + markAsReadOnScroll: Boolean, ) { Log.d("jerboa", "got to home activity") val transferCreatePostDepsViaRoot = appState.rootChannel() @@ -107,6 +110,10 @@ fun HomeActivity( if (homeViewModel.initialized) homeViewModel.updatePost(pv) } + appState.ConsumeReturn(PostViewReturn.POST_VIEW) { pv -> + if (homeViewModel.initialized) homeViewModel.updatePost(pv) + } + LaunchedEffect(account) { if (!account.isAnon() && !account.isReady()) { account.doIfReadyElseDisplayInfo(appState, ctx, snackbarHostState, scope, siteViewModel, accountViewModel) {} @@ -149,6 +156,7 @@ fun HomeActivity( usePrivateTabs = usePrivateTabs, blurNSFW = blurNSFW, showPostLinkPreviews = showPostLinkPreviews, + markAsReadOnScroll = markAsReadOnScroll, snackbarHostState = snackbarHostState, ) }, @@ -197,6 +205,7 @@ fun MainPostListingsContent( blurNSFW: Boolean, showPostLinkPreviews: Boolean, snackbarHostState: SnackbarHostState, + markAsReadOnScroll: Boolean, ) { val ctx = LocalContext.current val scope = rememberCoroutineScope() @@ -397,6 +406,19 @@ fun MainPostListingsContent( showPostLinkPreviews = showPostLinkPreviews, openImageViewer = appState::toView, openLink = appState::openLink, + markAsReadOnScroll = markAsReadOnScroll, + onMarkAsRead = { postView -> + if (!account.isAnon() && !postView.read) { + homeViewModel.markPostAsRead( + MarkPostAsRead( + post_id = postView.post.id, + read = true, + auth = account.jwt, + ), + appState, + ) + } + }, showIfRead = true, showScores = siteViewModel.showScores(), ) diff --git a/app/src/main/java/com/jerboa/ui/components/person/PersonProfileActivity.kt b/app/src/main/java/com/jerboa/ui/components/person/PersonProfileActivity.kt index 1a643c82f..cbd717f03 100644 --- a/app/src/main/java/com/jerboa/ui/components/person/PersonProfileActivity.kt +++ b/app/src/main/java/com/jerboa/ui/components/person/PersonProfileActivity.kt @@ -62,6 +62,7 @@ import com.jerboa.datatypes.types.CreatePostLike import com.jerboa.datatypes.types.DeleteComment import com.jerboa.datatypes.types.DeletePost import com.jerboa.datatypes.types.GetPersonDetails +import com.jerboa.datatypes.types.MarkPostAsRead import com.jerboa.datatypes.types.PersonId import com.jerboa.datatypes.types.PostView import com.jerboa.datatypes.types.SaveComment @@ -96,6 +97,7 @@ import com.jerboa.ui.components.common.isRefreshing import com.jerboa.ui.components.common.simpleVerticalScrollbar import com.jerboa.ui.components.community.CommunityLink import com.jerboa.ui.components.post.PostListings +import com.jerboa.ui.components.post.PostViewReturn import com.jerboa.ui.components.post.edit.PostEditReturn import com.jerboa.ui.theme.MEDIUM_PADDING import com.jerboa.util.InitializeRoute @@ -119,6 +121,7 @@ fun PersonProfileActivity( blurNSFW: Boolean, showPostLinkPreviews: Boolean, drawerState: DrawerState, + markAsReadOnScroll: Boolean, onBack: (() -> Unit)? = null, ) { Log.d("jerboa", "got to person activity") @@ -283,6 +286,7 @@ fun PersonProfileActivity( usePrivateTabs = usePrivateTabs, blurNSFW = blurNSFW, showPostLinkPreviews = showPostLinkPreviews, + markAsReadOnScroll = markAsReadOnScroll, snackbarHostState = snackbarHostState, showScores = siteViewModel.showScores(), ) @@ -315,6 +319,7 @@ fun UserTabs( usePrivateTabs: Boolean, blurNSFW: Boolean, showPostLinkPreviews: Boolean, + markAsReadOnScroll: Boolean, snackbarHostState: SnackbarHostState, showScores: Boolean, ) { @@ -334,6 +339,10 @@ fun UserTabs( val loading = personProfileViewModel.personDetailsRes.isLoading() + appState.ConsumeReturn(PostViewReturn.POST_VIEW) { pv -> + if (personProfileViewModel.initialized) personProfileViewModel.updatePost(pv) + } + val pullRefreshState = rememberPullRefreshState( refreshing = personProfileViewModel.personDetailsRes.isRefreshing(), onRefresh = { @@ -617,6 +626,19 @@ fun UserTabs( openImageViewer = appState::toView, openLink = appState::openLink, showPostLinkPreviews = showPostLinkPreviews, + markAsReadOnScroll = markAsReadOnScroll, + onMarkAsRead = { + if (!account.isAnon() && !it.read) { + personProfileViewModel.markPostAsRead( + MarkPostAsRead( + post_id = it.post.id, + read = true, + auth = account.jwt, + ), + appState, + ) + } + }, showIfRead = false, showScores = showScores, ) diff --git a/app/src/main/java/com/jerboa/ui/components/post/PostActivity.kt b/app/src/main/java/com/jerboa/ui/components/post/PostActivity.kt index 4ca1f2220..1fe592a8e 100644 --- a/app/src/main/java/com/jerboa/ui/components/post/PostActivity.kt +++ b/app/src/main/java/com/jerboa/ui/components/post/PostActivity.kt @@ -74,6 +74,7 @@ import com.jerboa.datatypes.types.PostId import com.jerboa.datatypes.types.PostView import com.jerboa.datatypes.types.SaveComment import com.jerboa.datatypes.types.SavePost +import com.jerboa.db.entity.isAnon import com.jerboa.getCommentParentId import com.jerboa.getDepthFromComment import com.jerboa.getLocalizedCommentSortTypeName @@ -105,6 +106,10 @@ import com.jerboa.ui.components.post.edit.PostEditReturn import com.jerboa.util.InitializeRoute import com.jerboa.util.doIfReadyElseDisplayInfo +object PostViewReturn { + const val POST_VIEW = "post-view::return(post-view)" +} + @Composable fun CommentsHeaderTitle( selectedSortType: CommentSortType, @@ -305,7 +310,7 @@ fun PostActivity( is ApiState.Failure -> ApiErrorText(postRes.msg) is ApiState.Success -> { val postView = postRes.data.post_view - + if (!account.isAnon()) appState.addReturn(PostViewReturn.POST_VIEW, postView.copy(read = true)) LazyColumn( state = listState, modifier = Modifier @@ -336,8 +341,6 @@ fun PostActivity( ), ) } - // TODO will need to pass in postlistingsviewmodel - // for the Home page to also be updated }, onDownvoteClick = { pv -> account.doIfReadyElseDisplayInfo( diff --git a/app/src/main/java/com/jerboa/ui/components/post/PostListing.kt b/app/src/main/java/com/jerboa/ui/components/post/PostListing.kt index 96b0ea338..d5e1238ba 100644 --- a/app/src/main/java/com/jerboa/ui/components/post/PostListing.kt +++ b/app/src/main/java/com/jerboa/ui/components/post/PostListing.kt @@ -483,7 +483,8 @@ fun PostBody( } } } else { - val defaultColor: Color = LocalContentColor.current.copy(alpha = LocalContentAlpha.current) + val defaultColor: Color = + LocalContentColor.current.copy(alpha = LocalContentAlpha.current) CreateMarkdownPreview( markdown = text, @@ -1662,10 +1663,23 @@ fun PostOptionsDialog( text = stringResource(R.string.post_listing_copy_thumbnail_link), icon = Icons.Outlined.Link, onClick = { - if (copyToClipboard(ctx, postView.post.thumbnail_url, "thumbnail link")) { - Toast.makeText(ctx, ctx.getString(R.string.post_listing_thumbnail_link_copied), Toast.LENGTH_SHORT).show() + if (copyToClipboard( + ctx, + postView.post.thumbnail_url, + "thumbnail link", + ) + ) { + Toast.makeText( + ctx, + ctx.getString(R.string.post_listing_thumbnail_link_copied), + Toast.LENGTH_SHORT, + ).show() } else { - Toast.makeText(ctx, ctx.getString(R.string.generic_error), Toast.LENGTH_SHORT).show() + Toast.makeText( + ctx, + ctx.getString(R.string.generic_error), + Toast.LENGTH_SHORT, + ).show() } onDismissRequest() }, @@ -1676,10 +1690,23 @@ fun PostOptionsDialog( text = stringResource(R.string.post_listing_copy_title), icon = Icons.Outlined.ContentCopy, onClick = { - if (copyToClipboard(ctx, postView.post.embed_description, "post title")) { - Toast.makeText(ctx, ctx.getString(R.string.post_listing_title_copied), Toast.LENGTH_SHORT).show() + if (copyToClipboard( + ctx, + postView.post.embed_description, + "post title", + ) + ) { + Toast.makeText( + ctx, + ctx.getString(R.string.post_listing_title_copied), + Toast.LENGTH_SHORT, + ).show() } else { - Toast.makeText(ctx, ctx.getString(R.string.generic_error), Toast.LENGTH_SHORT).show() + Toast.makeText( + ctx, + ctx.getString(R.string.generic_error), + Toast.LENGTH_SHORT, + ).show() } onDismissRequest() }, @@ -1691,9 +1718,17 @@ fun PostOptionsDialog( icon = Icons.Outlined.ContentCopy, onClick = { if (copyToClipboard(ctx, postView.post.name, "post name")) { - Toast.makeText(ctx, ctx.getString(R.string.post_listing_name_copied), Toast.LENGTH_SHORT).show() + Toast.makeText( + ctx, + ctx.getString(R.string.post_listing_name_copied), + Toast.LENGTH_SHORT, + ).show() } else { - Toast.makeText(ctx, ctx.getString(R.string.generic_error), Toast.LENGTH_SHORT).show() + Toast.makeText( + ctx, + ctx.getString(R.string.generic_error), + Toast.LENGTH_SHORT, + ).show() } onDismissRequest() }, @@ -1705,9 +1740,17 @@ fun PostOptionsDialog( icon = Icons.Outlined.ContentCopy, onClick = { if (copyToClipboard(ctx, postView.post.body, "post text")) { - Toast.makeText(ctx, ctx.getString(R.string.post_listing_text_copied), Toast.LENGTH_SHORT).show() + Toast.makeText( + ctx, + ctx.getString(R.string.post_listing_text_copied), + Toast.LENGTH_SHORT, + ).show() } else { - Toast.makeText(ctx, ctx.getString(R.string.generic_error), Toast.LENGTH_SHORT).show() + Toast.makeText( + ctx, + ctx.getString(R.string.generic_error), + Toast.LENGTH_SHORT, + ).show() } onDismissRequest() }, @@ -1715,7 +1758,13 @@ fun PostOptionsDialog( } postView.post.body?.also { IconAndTextDrawerItem( - text = if (viewSource) stringResource(R.string.post_listing_view_original) else stringResource(R.string.post_listing_view_source), + text = if (viewSource) { + stringResource(R.string.post_listing_view_original) + } else { + stringResource( + R.string.post_listing_view_source, + ) + }, icon = Icons.Outlined.Description, onClick = onViewSourceClick, ) diff --git a/app/src/main/java/com/jerboa/ui/components/post/PostListings.kt b/app/src/main/java/com/jerboa/ui/components/post/PostListings.kt index 8cfd7f8f5..4301f2646 100644 --- a/app/src/main/java/com/jerboa/ui/components/post/PostListings.kt +++ b/app/src/main/java/com/jerboa/ui/components/post/PostListings.kt @@ -5,10 +5,11 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.Divider import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue @@ -62,6 +63,8 @@ fun PostListings( showPostLinkPreviews: Boolean, openImageViewer: (url: String) -> Unit, openLink: (String, Boolean, Boolean) -> Unit, + markAsReadOnScroll: Boolean, + onMarkAsRead: (postView: PostView) -> Unit, showIfRead: Boolean, showScores: Boolean, ) { @@ -77,13 +80,10 @@ fun PostListings( contentAboveListings() } // List of items - items( - posts, - key = { postView -> - postView.post.id - }, - contentType = { "Post" }, - ) { postView -> + itemsIndexed( + items = posts, + contentType = { _, _ -> "Post" }, + ) { index, postView -> PostListing( postView = postView, onUpvoteClick = onUpvoteClick, @@ -114,7 +114,17 @@ fun PostListings( openLink = openLink, showIfRead = showIfRead, showScores = showScores, - ) + ).let { + if (!postView.read && markAsReadOnScroll) { + DisposableEffect(key1 = postView.post.id) { + onDispose { + if (listState.isScrollInProgress && index < listState.firstVisibleItemIndex) { + onMarkAsRead(postView) + } + } + } + } + } Divider(modifier = Modifier.padding(bottom = SMALL_PADDING)) } } @@ -164,6 +174,8 @@ fun PreviewPostListings() { showPostLinkPreviews = true, openImageViewer = {}, openLink = { _: String, _: Boolean, _: Boolean -> }, + markAsReadOnScroll = false, + onMarkAsRead = {}, showIfRead = true, showScores = true, ) diff --git a/app/src/main/java/com/jerboa/ui/components/settings/account/AccountSettings.kt b/app/src/main/java/com/jerboa/ui/components/settings/account/AccountSettings.kt index 10734cb57..2cd4bc23c 100644 --- a/app/src/main/java/com/jerboa/ui/components/settings/account/AccountSettings.kt +++ b/app/src/main/java/com/jerboa/ui/components/settings/account/AccountSettings.kt @@ -5,7 +5,7 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.* +import androidx.compose.material.icons.outlined.Close import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable @@ -32,7 +32,7 @@ import com.jerboa.imageInputStreamFromUri import com.jerboa.model.AccountSettingsViewModel import com.jerboa.model.SiteViewModel import com.jerboa.ui.components.common.* -import com.jerboa.ui.theme.* +import com.jerboa.ui.theme.MEDIUM_PADDING import kotlinx.coroutines.launch @Composable @@ -262,6 +262,7 @@ fun SettingsForm( Text(text = stringResource(R.string.account_settings_show_read_posts)) }, ) + SettingsCheckbox( state = botAccount, title = { diff --git a/app/src/main/java/com/jerboa/ui/components/settings/lookandfeel/LookAndFeelActivity.kt b/app/src/main/java/com/jerboa/ui/components/settings/lookandfeel/LookAndFeelActivity.kt index c51317ec8..df08a5259 100644 --- a/app/src/main/java/com/jerboa/ui/components/settings/lookandfeel/LookAndFeelActivity.kt +++ b/app/src/main/java/com/jerboa/ui/components/settings/lookandfeel/LookAndFeelActivity.kt @@ -71,14 +71,9 @@ fun LookAndFeelActivity( val postViewModeState = rememberIntSettingState(settings.postViewMode) val showBottomNavState = rememberBooleanSettingState(settings.showBottomNav) val showTextDescriptionsInNavbar = rememberBooleanSettingState(settings.showTextDescriptionsInNavbar) - val showCollapsedCommentContentState = - rememberBooleanSettingState(settings.showCollapsedCommentContent) - val showCommentActionBarByDefaultState = rememberBooleanSettingState( - settings.showCommentActionBarByDefault, - ) - val showVotingArrowsInListViewState = rememberBooleanSettingState( - settings.showVotingArrowsInListView, - ) + val showCollapsedCommentContentState = rememberBooleanSettingState(settings.showCollapsedCommentContent) + val showCommentActionBarByDefaultState = rememberBooleanSettingState(settings.showCommentActionBarByDefault) + val showVotingArrowsInListViewState = rememberBooleanSettingState(settings.showVotingArrowsInListView) val showParentCommentNavigationButtonsState = rememberBooleanSettingState( settings.showParentCommentNavigationButtons, ) @@ -97,6 +92,8 @@ fun LookAndFeelActivity( val scrollState = rememberScrollState() + val markAsReadOnScroll = rememberBooleanSettingState(settings.markAsReadOnScroll) + fun updateAppSettings() { appSettingsViewModel.update( AppSettings( @@ -119,6 +116,7 @@ fun LookAndFeelActivity( blurNSFW = blurNSFW.value, backConfirmationMode = backConfirmationMode.value, showPostLinkPreviews = showPostLinkPreviewMode.value, + markAsReadOnScroll = markAsReadOnScroll.value, ), ) } @@ -308,6 +306,13 @@ fun LookAndFeelActivity( }, onCheckedChange = { updateAppSettings() }, ) + SettingsCheckbox( + state = markAsReadOnScroll, + title = { + Text(stringResource(id = R.string.mark_as_read_on_scroll)) + }, + onCheckedChange = { updateAppSettings() }, + ) SettingsList( title = { Text(text = stringResource(R.string.confirm_exit)) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 85ea67d9f..1c46cb1e7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -347,6 +347,7 @@ Failed to save image! Switch to anonymous You are already logged in with this account + Mark as read on scroll %1$s@%2$s Info Send a message Sending message failed, retrying... @@ -374,4 +375,5 @@ Incorrect TOTP token given This instance (%s) is experiencing internal server errors Your account is banned until %s +