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.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.manageaccounts.di.manageAccountsModule
import com.github.diegoberaldin.raccoonforlemmy.unit.myaccount.di.myAccountModule
@ -19,4 +21,9 @@ val profileTabModule = module {
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.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import cafe.adriel.voyager.koin.getScreenModel
import cafe.adriel.voyager.navigator.tab.Tab
import cafe.adriel.voyager.navigator.tab.TabOptions
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.Spacing
@ -28,23 +31,41 @@ internal object ProfileNotLoggedScreen : Tab {
@Composable
override fun Content() {
val model = getScreenModel<ProfileNotLoggedMviModel>()
val navigationCoordinator = remember { getNavigationCoordinator() }
val uiState by model.uiState.collectAsState()
Column(
modifier = Modifier.fillMaxSize().padding(horizontal = Spacing.m),
verticalArrangement = Arrangement.spacedBy(Spacing.xs),
) {
Text(
text = LocalXmlStrings.current.profileNotLoggedMessage,
)
val message = if (uiState.authError) {
LocalXmlStrings.current.messageAuthIssue
} else {
LocalXmlStrings.current.profileNotLoggedMessage
}
Text(text = message)
Spacer(modifier = Modifier.height(Spacing.l))
Button(
modifier = Modifier.align(Alignment.CenterHorizontally),
onClick = {
navigationCoordinator.pushScreen(LoginBottomSheet())
},
) {
Text(LocalXmlStrings.current.profileButtonLogin)
if (uiState.authError) {
Button(
modifier = Modifier.align(Alignment.CenterHorizontally),
onClick = {
model.reduce(ProfileNotLoggedMviModel.Intent.Retry)
},
) {
Text(LocalXmlStrings.current.buttonRetry)
}
} else {
Button(
modifier = Modifier.align(Alignment.CenterHorizontally),
onClick = {
navigationCoordinator.pushScreen(LoginBottomSheet())
},
) {
Text(LocalXmlStrings.current.profileButtonLogin)
}
}
}
}

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 {
sealed interface Intent {
data object Refresh : Intent
data class Refresh(val hardReset: Boolean = false) : Intent
data object LoadNextPage : Intent
data class ChangeListing(val value: ListingType) : 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.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
@ -294,7 +295,7 @@ class PostListScreen : Screen {
val pullRefreshState = rememberPullRefreshState(
refreshing = uiState.refreshing,
onRefresh = rememberCallback(model) {
model.reduce(PostListMviModel.Intent.Refresh)
model.reduce(PostListMviModel.Intent.Refresh())
},
)
Box(
@ -637,7 +638,10 @@ class PostListScreen : Screen {
PostListMviModel.Intent.Copy(texts.first())
)
} else {
val screen = CopyPostBottomSheet(post.title, post.text)
val screen = CopyPostBottomSheet(
post.title,
post.text,
)
navigationCoordinator.showBottomSheet(screen)
}
}
@ -715,6 +719,29 @@ class PostListScreen : Screen {
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)
identityRepository.isLogged.onEach { logged ->
refreshUser()
updateState {
it.copy(isLogged = logged ?: false)
}
@ -150,10 +151,6 @@ class PostListViewModel(
emitEffect(PostListMviModel.Effect.ZombieModeTick(index))
}
}.launchIn(this)
val auth = identityRepository.authToken.value.orEmpty()
val user = siteRepository.getCurrentUser(auth)
updateState { it.copy(currentUserId = user?.id ?: 0) }
}
onFirstLoad()
@ -168,11 +165,18 @@ class PostListViewModel(
)
}
screenModelScope.launch {
refreshUser()
refresh(initial = true)
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() {
val sortTypes = getSortTypesUseCase.getTypesForPosts()
updateState { it.copy(availableSortTypes = sortTypes) }
@ -184,7 +188,10 @@ class PostListViewModel(
loadNextPage()
}
PostListMviModel.Intent.Refresh -> screenModelScope.launch {
is PostListMviModel.Intent.Refresh -> screenModelScope.launch {
if (intent.hardReset) {
refreshUser()
}
refresh()
}
@ -292,7 +299,6 @@ class PostListViewModel(
return
}
updateState { it.copy(loading = true) }
val refreshing = currentState.refreshing
val posts = postPaginationManager.loadNextPage().let {
if (!hideReadPosts) {
it