diff --git a/app/src/main/java/com/jerboa/api/Http.kt b/app/src/main/java/com/jerboa/api/Http.kt index 9e7f87e4e..c80bc4c90 100644 --- a/app/src/main/java/com/jerboa/api/Http.kt +++ b/app/src/main/java/com/jerboa/api/Http.kt @@ -362,10 +362,11 @@ sealed class ApiState { abstract class Holder(val data: T) : ApiState() class Success(data: T) : Holder(data) class Appending(data: T) : Holder(data) + class AppendingFailure(data: T) : Holder(data) class Failure(val msg: Throwable) : ApiState() - object Loading : ApiState() - object Refreshing : ApiState() - object Empty : ApiState() + data object Loading : ApiState() + data object Refreshing : ApiState() + data object Empty : ApiState() } fun apiWrapper( diff --git a/app/src/main/java/com/jerboa/model/CommunityViewModel.kt b/app/src/main/java/com/jerboa/model/CommunityViewModel.kt index fec4335fd..b2468ddcb 100644 --- a/app/src/main/java/com/jerboa/model/CommunityViewModel.kt +++ b/app/src/main/java/com/jerboa/model/CommunityViewModel.kt @@ -101,8 +101,9 @@ class CommunityViewModel : ViewModel(), Initializable { fun appendPosts(id: CommunityId, jwt: String?) { viewModelScope.launch { val oldRes = postsRes - when (oldRes) { - is ApiState.Success -> postsRes = ApiState.Appending(oldRes.data) + postsRes = when (oldRes) { + is ApiState.Appending -> return@launch + is ApiState.Holder -> ApiState.Appending(oldRes.data) else -> return@launch } @@ -118,9 +119,6 @@ class CommunityViewModel : ViewModel(), Initializable { postsRes = when (newRes) { is ApiState.Success -> { - if (newRes.data.posts.isEmpty()) { // Hit the end of the posts - prevPage() - } ApiState.Success( GetPostsResponse( mergePosts( @@ -133,7 +131,7 @@ class CommunityViewModel : ViewModel(), Initializable { else -> { prevPage() - oldRes + ApiState.AppendingFailure(oldRes.data) } } } diff --git a/app/src/main/java/com/jerboa/model/HomeViewModel.kt b/app/src/main/java/com/jerboa/model/HomeViewModel.kt index b3152805c..ecc8433ef 100644 --- a/app/src/main/java/com/jerboa/model/HomeViewModel.kt +++ b/app/src/main/java/com/jerboa/model/HomeViewModel.kt @@ -109,8 +109,9 @@ class HomeViewModel(private val accountRepository: AccountRepository) : ViewMode fun appendPosts(jwt: String?) { viewModelScope.launch { val oldRes = postsRes - when (oldRes) { - is ApiState.Success -> postsRes = ApiState.Appending(oldRes.data) + postsRes = when (oldRes) { + is ApiState.Appending -> return@launch + is ApiState.Holder -> ApiState.Appending(oldRes.data) else -> return@launch } @@ -119,9 +120,6 @@ class HomeViewModel(private val accountRepository: AccountRepository) : ViewMode postsRes = when (newRes) { is ApiState.Success -> { - if (newRes.data.posts.isEmpty()) { // Hit the end of the posts - prevPage() - } ApiState.Success( GetPostsResponse( mergePosts( @@ -134,7 +132,7 @@ class HomeViewModel(private val accountRepository: AccountRepository) : ViewMode else -> { prevPage() - oldRes + ApiState.AppendingFailure(oldRes.data) } } } diff --git a/app/src/main/java/com/jerboa/model/InboxViewModel.kt b/app/src/main/java/com/jerboa/model/InboxViewModel.kt index 3f2bb4b9b..7ff67fda7 100644 --- a/app/src/main/java/com/jerboa/model/InboxViewModel.kt +++ b/app/src/main/java/com/jerboa/model/InboxViewModel.kt @@ -133,9 +133,6 @@ class InboxViewModel : ViewModel(), Initializable { repliesRes = when (newRes) { is ApiState.Success -> { - if (newRes.data.replies.isEmpty()) { // Hit the end of the replies - pageReplies -= 1 - } val appended = oldRes.data.replies.toMutableList() appended.addAll(newRes.data.replies) ApiState.Success(oldRes.data.copy(replies = appended)) @@ -181,9 +178,6 @@ class InboxViewModel : ViewModel(), Initializable { mentionsRes = when (newRes) { is ApiState.Success -> { - if (newRes.data.mentions.isEmpty()) { // Hit the end of the replies - pageMentions -= 1 - } val appended = oldRes.data.mentions.toMutableList() appended.addAll(newRes.data.mentions) ApiState.Success(oldRes.data.copy(mentions = appended)) @@ -228,9 +222,6 @@ class InboxViewModel : ViewModel(), Initializable { messagesRes = when (newRes) { is ApiState.Success -> { - if (newRes.data.private_messages.isEmpty()) { // Hit the end of the replies - pageMessages -= 1 - } val appended = oldRes.data.private_messages.toMutableList() appended.addAll(newRes.data.private_messages) ApiState.Success(oldRes.data.copy(private_messages = appended)) diff --git a/app/src/main/java/com/jerboa/model/PersonProfileViewModel.kt b/app/src/main/java/com/jerboa/model/PersonProfileViewModel.kt index 0a5f7b1f4..003493782 100644 --- a/app/src/main/java/com/jerboa/model/PersonProfileViewModel.kt +++ b/app/src/main/java/com/jerboa/model/PersonProfileViewModel.kt @@ -23,7 +23,6 @@ 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 @@ -42,18 +41,13 @@ import kotlinx.coroutines.launch class PersonProfileViewModel : ViewModel(), Initializable { override var initialized by mutableStateOf(false) - var personDetailsRes: ApiState by mutableStateOf( - ApiState.Empty, - ) + var personDetailsRes: 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 blockCommunityRes: ApiState by mutableStateOf(ApiState.Empty) private var blockPersonRes: ApiState by mutableStateOf(ApiState.Empty) private var likeCommentRes: ApiState by mutableStateOf(ApiState.Empty) @@ -108,8 +102,9 @@ class PersonProfileViewModel : ViewModel(), Initializable { ) { viewModelScope.launch { val oldRes = personDetailsRes - when (oldRes) { - is ApiState.Success -> personDetailsRes = ApiState.Appending(oldRes.data) + personDetailsRes = when (oldRes) { + is ApiState.Appending -> return@launch + is ApiState.Holder -> ApiState.Appending(oldRes.data) else -> return@launch } @@ -141,7 +136,7 @@ class PersonProfileViewModel : ViewModel(), Initializable { else -> { prevPage() - oldRes + ApiState.AppendingFailure(oldRes.data) } } } diff --git a/app/src/main/java/com/jerboa/ui/components/common/ApiStateHelpers.kt b/app/src/main/java/com/jerboa/ui/components/common/ApiStateHelpers.kt index 1a44eb4a9..359d15e2a 100644 --- a/app/src/main/java/com/jerboa/ui/components/common/ApiStateHelpers.kt +++ b/app/src/main/java/com/jerboa/ui/components/common/ApiStateHelpers.kt @@ -2,16 +2,26 @@ package com.jerboa.ui.components.common import android.content.Context import android.widget.Toast +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.padding import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier import com.jerboa.api.ApiState @Composable fun ApiErrorText( msg: Throwable, + paddingValues: PaddingValues = PaddingValues(), ) { - msg.message?.also { Text(text = it, color = MaterialTheme.colorScheme.error) } + msg.message?.also { + Text( + text = it, + modifier = Modifier.padding(paddingValues), + color = MaterialTheme.colorScheme.error, + ) + } } fun apiErrorToast( diff --git a/app/src/main/java/com/jerboa/ui/components/common/Buttons.kt b/app/src/main/java/com/jerboa/ui/components/common/Buttons.kt new file mode 100644 index 000000000..b300acf6f --- /dev/null +++ b/app/src/main/java/com/jerboa/ui/components/common/Buttons.kt @@ -0,0 +1,29 @@ +package com.jerboa.ui.components.common + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import com.jerboa.R +import com.jerboa.ui.theme.XXL_PADDING + +@Composable +fun RetryLoadingPosts(onClick: () -> Unit) { + Button( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = XXL_PADDING), + onClick = onClick, + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.errorContainer, + contentColor = MaterialTheme.colorScheme.onErrorContainer, + ), + ) { + Text(stringResource(R.string.posts_failed_loading)) + } +} 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 af01f9c7c..b7e9f3df3 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 @@ -448,7 +448,7 @@ fun CommunityActivity( onShareClick = { url -> shareLink(url, ctx) }, - isScrolledToEnd = { + loadMorePosts = { when (val communityRes = communityViewModel.communityRes) { is ApiState.Success -> { communityViewModel.appendPosts( @@ -465,9 +465,9 @@ fun CommunityActivity( padding = padding, listState = postListState, postViewMode = getPostViewMode(appSettingsViewModel), + showVotingArrowsInListView = showVotingArrowsInListView, enableDownVotes = siteViewModel.enableDownvotes(), showAvatar = siteViewModel.showAvatar(), - showVotingArrowsInListView = showVotingArrowsInListView, useCustomTabs = useCustomTabs, usePrivateTabs = usePrivateTabs, blurNSFW = blurNSFW, @@ -489,6 +489,7 @@ fun CommunityActivity( showIfRead = true, showScores = siteViewModel.showScores(), postActionbarMode = postActionbarMode, + showPostAppendRetry = communityViewModel.postsRes is ApiState.AppendingFailure, ) } else -> {} 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 74e21f4de..7292b7aa8 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 @@ -219,7 +219,7 @@ fun MainPostListingsContent( when (val siteRes = siteViewModel.siteRes) { ApiState.Loading -> LoadingBar(padding) ApiState.Empty -> ApiEmptyText() - is ApiState.Failure -> ApiErrorText(siteRes.msg) + is ApiState.Failure -> ApiErrorText(siteRes.msg, padding) is ApiState.Success -> { taglines = siteRes.data.taglines } @@ -391,13 +391,11 @@ fun MainPostListingsContent( onCommunityClick = { community -> appState.toCommunity(id = community.id) }, - onPersonClick = { personId -> - appState.toProfile(id = personId) - }, + onPersonClick = appState::toProfile, onShareClick = { url -> shareLink(url, ctx) }, - isScrolledToEnd = { + loadMorePosts = { homeViewModel.appendPosts(account.jwt) }, account = account, @@ -425,6 +423,7 @@ fun MainPostListingsContent( showIfRead = true, showScores = siteViewModel.showScores(), postActionbarMode = postActionbarMode, + showPostAppendRetry = homeViewModel.postsRes is ApiState.AppendingFailure, ) } } 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 695622036..63e30c7f2 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 @@ -616,7 +616,7 @@ fun UserTabs( onShareClick = { url -> shareLink(url, ctx) }, - isScrolledToEnd = { + loadMorePosts = { personProfileViewModel.appendData( profileRes.data.person_view.person.id, account.getJWT(), @@ -625,14 +625,14 @@ fun UserTabs( account = account, listState = postListState, postViewMode = getPostViewMode(appSettingsViewModel), + showVotingArrowsInListView = showVotingArrowsInListView, enableDownVotes = enableDownVotes, showAvatar = showAvatar, - showVotingArrowsInListView = showVotingArrowsInListView, useCustomTabs = useCustomTabs, usePrivateTabs = usePrivateTabs, blurNSFW = blurNSFW, - appState = appState, showPostLinkPreviews = showPostLinkPreviews, + appState = appState, markAsReadOnScroll = markAsReadOnScroll, onMarkAsRead = { if (!account.isAnon() && !it.read) { @@ -649,6 +649,7 @@ fun UserTabs( showIfRead = false, showScores = showScores, postActionbarMode = postActionbarMode, + showPostAppendRetry = personProfileViewModel.personDetailsRes is ApiState.AppendingFailure, ) } else -> {} 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 031118bbf..320ca8481 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 @@ -29,6 +29,7 @@ import com.jerboa.db.entity.Account import com.jerboa.db.entity.AnonAccount import com.jerboa.isScrolledToEnd import com.jerboa.rememberJerboaAppState +import com.jerboa.ui.components.common.RetryLoadingPosts import com.jerboa.ui.components.common.simpleVerticalScrollbar import com.jerboa.ui.theme.SMALL_PADDING import kotlinx.collections.immutable.ImmutableList @@ -50,7 +51,7 @@ fun PostListings( onBlockCommunityClick: (community: Community) -> Unit, onBlockCreatorClick: (person: Person) -> Unit, onShareClick: (url: String) -> Unit, - isScrolledToEnd: () -> Unit, + loadMorePosts: () -> Unit, account: Account, showCommunityName: Boolean = true, padding: PaddingValues = PaddingValues(0.dp), @@ -69,6 +70,7 @@ fun PostListings( showIfRead: Boolean, showScores: Boolean, postActionbarMode: Int, + showPostAppendRetry: Boolean, ) { LazyColumn( state = listState, @@ -129,6 +131,12 @@ fun PostListings( } Divider(modifier = Modifier.padding(bottom = SMALL_PADDING)) } + + if (showPostAppendRetry) { + item(contentType = "retry_posts") { + RetryLoadingPosts(loadMorePosts) + } + } } // observer when reached end of list @@ -139,9 +147,9 @@ fun PostListings( } // Act when end of list reached - if (endOfListReached) { + if (endOfListReached && !showPostAppendRetry) { LaunchedEffect(Unit) { - isScrolledToEnd() + loadMorePosts() } } } @@ -163,7 +171,7 @@ fun PreviewPostListings() { onBlockCommunityClick = {}, onBlockCreatorClick = {}, onShareClick = {}, - isScrolledToEnd = {}, + loadMorePosts = {}, account = AnonAccount, listState = rememberLazyListState(), postViewMode = PostViewMode.Card, @@ -180,5 +188,6 @@ fun PreviewPostListings() { showIfRead = true, showScores = true, postActionbarMode = 0, + showPostAppendRetry = false, ) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 236d0b503..5bca50f86 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -413,5 +413,6 @@ No activity (app) found that can open this link Insert spoiler Connect on Matrix + Posts failed loading, retry Failed to share media!