enhancement: retry button for network failure (#786)

This commit is contained in:
Diego Beraldin 2024-05-04 23:46:35 +02:00 committed by GitHub
parent 0f7ff3e5a0
commit d58d2ba4b4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 129 additions and 19 deletions

View File

@ -2,6 +2,8 @@ package com.github.diegoberaldin.raccoonforlemmy.feature.profile.di
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.main.ProfileMainMviModel import com.github.diegoberaldin.raccoonforlemmy.feature.profile.main.ProfileMainMviModel
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.main.ProfileMainViewModel import com.github.diegoberaldin.raccoonforlemmy.feature.profile.main.ProfileMainViewModel
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.notlogged.ProfileNotLoggedMviModel
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.notlogged.ProfileNotLoggedViewModel
import com.github.diegoberaldin.raccoonforlemmy.unit.login.di.loginModule import com.github.diegoberaldin.raccoonforlemmy.unit.login.di.loginModule
import com.github.diegoberaldin.raccoonforlemmy.unit.manageaccounts.di.manageAccountsModule import com.github.diegoberaldin.raccoonforlemmy.unit.manageaccounts.di.manageAccountsModule
import com.github.diegoberaldin.raccoonforlemmy.unit.myaccount.di.myAccountModule import com.github.diegoberaldin.raccoonforlemmy.unit.myaccount.di.myAccountModule
@ -19,4 +21,9 @@ val profileTabModule = module {
logout = get(), logout = get(),
) )
} }
factory<ProfileNotLoggedMviModel> {
ProfileNotLoggedViewModel(
identityRepository = get(),
)
}
} }

View File

@ -0,0 +1,16 @@
package com.github.diegoberaldin.raccoonforlemmy.feature.profile.notlogged
import cafe.adriel.voyager.core.model.ScreenModel
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.MviModel
interface ProfileNotLoggedMviModel :
ScreenModel,
MviModel<ProfileNotLoggedMviModel.Intent, ProfileNotLoggedMviModel.State, ProfileNotLoggedMviModel.Effect> {
sealed interface Intent {
data object Retry : Intent
}
data class State(val authError: Boolean = false)
sealed interface Effect
}

View File

@ -9,9 +9,12 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import cafe.adriel.voyager.koin.getScreenModel
import cafe.adriel.voyager.navigator.tab.Tab import cafe.adriel.voyager.navigator.tab.Tab
import cafe.adriel.voyager.navigator.tab.TabOptions import cafe.adriel.voyager.navigator.tab.TabOptions
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.Spacing import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.Spacing
@ -28,16 +31,33 @@ internal object ProfileNotLoggedScreen : Tab {
@Composable @Composable
override fun Content() { override fun Content() {
val model = getScreenModel<ProfileNotLoggedMviModel>()
val navigationCoordinator = remember { getNavigationCoordinator() } val navigationCoordinator = remember { getNavigationCoordinator() }
val uiState by model.uiState.collectAsState()
Column( Column(
modifier = Modifier.fillMaxSize().padding(horizontal = Spacing.m), modifier = Modifier.fillMaxSize().padding(horizontal = Spacing.m),
verticalArrangement = Arrangement.spacedBy(Spacing.xs), verticalArrangement = Arrangement.spacedBy(Spacing.xs),
) { ) {
Text( val message = if (uiState.authError) {
text = LocalXmlStrings.current.profileNotLoggedMessage, LocalXmlStrings.current.messageAuthIssue
) } else {
LocalXmlStrings.current.profileNotLoggedMessage
}
Text(text = message)
Spacer(modifier = Modifier.height(Spacing.l)) Spacer(modifier = Modifier.height(Spacing.l))
if (uiState.authError) {
Button(
modifier = Modifier.align(Alignment.CenterHorizontally),
onClick = {
model.reduce(ProfileNotLoggedMviModel.Intent.Retry)
},
) {
Text(LocalXmlStrings.current.buttonRetry)
}
} else {
Button( Button(
modifier = Modifier.align(Alignment.CenterHorizontally), modifier = Modifier.align(Alignment.CenterHorizontally),
onClick = { onClick = {
@ -48,4 +68,5 @@ internal object ProfileNotLoggedScreen : Tab {
} }
} }
} }
}
} }

View File

@ -0,0 +1,33 @@
package com.github.diegoberaldin.raccoonforlemmy.feature.profile.notlogged
import cafe.adriel.voyager.core.model.screenModelScope
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.DefaultMviModel
import com.github.diegoberaldin.raccoonforlemmy.domain.identity.repository.IdentityRepository
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
class ProfileNotLoggedViewModel(
private val identityRepository: IdentityRepository,
) : ProfileNotLoggedMviModel,
DefaultMviModel<ProfileNotLoggedMviModel.Intent, ProfileNotLoggedMviModel.State, ProfileNotLoggedMviModel.Effect>(
initialState = ProfileNotLoggedMviModel.State()
) {
init {
screenModelScope.launch {
identityRepository.isLogged.onEach { logged ->
val auth = identityRepository.authToken.value
updateState { it.copy(authError = !auth.isNullOrEmpty() && logged == false) }
}.launchIn(this)
}
}
override fun reduce(intent: ProfileNotLoggedMviModel.Intent) {
when (intent) {
ProfileNotLoggedMviModel.Intent.Retry -> screenModelScope.launch {
identityRepository.refreshLoggedState()
}
}
}
}

View File

@ -16,7 +16,7 @@ interface PostListMviModel :
ScreenModel { ScreenModel {
sealed interface Intent { sealed interface Intent {
data object Refresh : Intent data class Refresh(val hardReset: Boolean = false) : Intent
data object LoadNextPage : Intent data object LoadNextPage : Intent
data class ChangeListing(val value: ListingType) : Intent data class ChangeListing(val value: ListingType) : Intent
data class UpVotePost(val id: Long, val feedback: Boolean = false) : Intent data class UpVotePost(val id: Long, val feedback: Boolean = false) : Intent

View File

@ -8,6 +8,7 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.animateScrollBy import androidx.compose.foundation.gestures.animateScrollBy
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
@ -294,7 +295,7 @@ class PostListScreen : Screen {
val pullRefreshState = rememberPullRefreshState( val pullRefreshState = rememberPullRefreshState(
refreshing = uiState.refreshing, refreshing = uiState.refreshing,
onRefresh = rememberCallback(model) { onRefresh = rememberCallback(model) {
model.reduce(PostListMviModel.Intent.Refresh) model.reduce(PostListMviModel.Intent.Refresh())
}, },
) )
Box( Box(
@ -637,7 +638,10 @@ class PostListScreen : Screen {
PostListMviModel.Intent.Copy(texts.first()) PostListMviModel.Intent.Copy(texts.first())
) )
} else { } else {
val screen = CopyPostBottomSheet(post.title, post.text) val screen = CopyPostBottomSheet(
post.title,
post.text,
)
navigationCoordinator.showBottomSheet(screen) navigationCoordinator.showBottomSheet(screen)
} }
} }
@ -715,6 +719,29 @@ class PostListScreen : Screen {
contentColor = MaterialTheme.colorScheme.onBackground, contentColor = MaterialTheme.colorScheme.onBackground,
) )
} }
} else {
Column(
modifier = Modifier.padding(vertical = Spacing.m),
verticalArrangement = Arrangement.spacedBy(Spacing.s),
) {
Text(
modifier = Modifier
.fillMaxWidth()
.padding(top = Spacing.xs),
textAlign = TextAlign.Center,
text = LocalXmlStrings.current.messageGenericError,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onBackground,
)
Button(
modifier = Modifier.align(Alignment.CenterHorizontally),
onClick = rememberCallback {
model.reduce(PostListMviModel.Intent.Refresh(hardReset = true))
},
) {
Text(LocalXmlStrings.current.buttonRetry)
}
}
} }
} }

View File

@ -66,6 +66,7 @@ class PostListViewModel(
}.launchIn(this) }.launchIn(this)
identityRepository.isLogged.onEach { logged -> identityRepository.isLogged.onEach { logged ->
refreshUser()
updateState { updateState {
it.copy(isLogged = logged ?: false) it.copy(isLogged = logged ?: false)
} }
@ -150,10 +151,6 @@ class PostListViewModel(
emitEffect(PostListMviModel.Effect.ZombieModeTick(index)) emitEffect(PostListMviModel.Effect.ZombieModeTick(index))
} }
}.launchIn(this) }.launchIn(this)
val auth = identityRepository.authToken.value.orEmpty()
val user = siteRepository.getCurrentUser(auth)
updateState { it.copy(currentUserId = user?.id ?: 0) }
} }
onFirstLoad() onFirstLoad()
@ -168,11 +165,18 @@ class PostListViewModel(
) )
} }
screenModelScope.launch { screenModelScope.launch {
refreshUser()
refresh(initial = true) refresh(initial = true)
emitEffect(PostListMviModel.Effect.BackToTop) emitEffect(PostListMviModel.Effect.BackToTop)
} }
} }
private suspend fun refreshUser() {
val auth = identityRepository.authToken.value.orEmpty()
val user = siteRepository.getCurrentUser(auth)
updateState { it.copy(currentUserId = user?.id ?: 0) }
}
private suspend fun updateAvailableSortTypes() { private suspend fun updateAvailableSortTypes() {
val sortTypes = getSortTypesUseCase.getTypesForPosts() val sortTypes = getSortTypesUseCase.getTypesForPosts()
updateState { it.copy(availableSortTypes = sortTypes) } updateState { it.copy(availableSortTypes = sortTypes) }
@ -184,7 +188,10 @@ class PostListViewModel(
loadNextPage() loadNextPage()
} }
PostListMviModel.Intent.Refresh -> screenModelScope.launch { is PostListMviModel.Intent.Refresh -> screenModelScope.launch {
if (intent.hardReset) {
refreshUser()
}
refresh() refresh()
} }
@ -292,7 +299,6 @@ class PostListViewModel(
return return
} }
updateState { it.copy(loading = true) } updateState { it.copy(loading = true) }
val refreshing = currentState.refreshing
val posts = postPaginationManager.loadNextPage().let { val posts = postPaginationManager.loadNextPage().let {
if (!hideReadPosts) { if (!hideReadPosts) {
it it