Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ private fun StatefulContentLoadingView() {
@Composable
private fun StatefulContentLoadingErrorView() {
val state = remember {
mutableStateOf(ContentLoadingErrorState.Loading)
mutableStateOf<ContentLoadingErrorState>(ContentLoadingErrorState.Loading)
}

ContentLoadingErrorView(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ internal fun ContentLoadingErrorViewErrorPreview() {
internal fun ContentLoadingErrorViewInteractivePreview() {
PreviewWithThemes {
val state = remember {
mutableStateOf(ContentLoadingErrorState.Loading)
mutableStateOf<ContentLoadingErrorState>(ContentLoadingErrorState.Loading)
}

DefaultContentLoadingErrorView(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,34 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier

/**
* A container view that can animate between a loading view, an error view, and a content view.
*
* @param ERROR The type describing the error.
* @param STATE The type of the state being passed to this view.
*
* @param state The state relevant for displaying the content inside this view.
* @param loading When `state.isLoading` is `true`, this composable function is displayed.
* @param error When `state.isLoading` is `false` and `state.error` is not `null`, this composable function is displayed
* with `state.error` being passed as the argument.
* @param content When `state.isLoading` is `false` and `state.error` is `null`, this composable function is displayed
* with [state] being passed as the argument.
*
* **IMPORTANT**: This is a delicate API whose usage should be carefully reviewed. It is using [AnimatedContent] and
* inherits its caveats.
*
* The [loading], [error] and [content] composable functions should only use the state being passed to them (if any).
* If you disregard this advice, make sure to read the documentation of [AnimatedContent] to learn when the composable
* functions are invoked and what that means for the external state a function fetches.
*/
@Composable
fun ContentLoadingErrorView(
state: ContentLoadingErrorState,
fun <ERROR, STATE : LoadingErrorState<ERROR>> ContentLoadingErrorView(
state: STATE,
loading: @Composable () -> Unit,
error: @Composable () -> Unit,
error: @Composable (ERROR) -> Unit,
modifier: Modifier = Modifier,
contentAlignment: Alignment = Alignment.Center,
content: @Composable () -> Unit,
content: @Composable (STATE) -> Unit,
) {
Box(
modifier = modifier,
Expand All @@ -22,18 +42,42 @@ fun ContentLoadingErrorView(
AnimatedContent(
targetState = state,
label = "ContentLoadingErrorView",
contentKey = { targetState ->
ContentKey(isLoading = targetState.isLoading, error = targetState.error)
},
) { targetState ->
when (targetState) {
ContentLoadingErrorState.Loading -> loading()
ContentLoadingErrorState.Content -> content()
ContentLoadingErrorState.Error -> error()
val errorValue = targetState.error
when {
targetState.isLoading -> loading()
errorValue != null -> error(errorValue)
else -> content(targetState)
}
}
}
}

enum class ContentLoadingErrorState {
Loading,
Content,
Error,
/**
* Signals [ContentLoadingErrorView] which of its composable function parameters to execute/display.
*/
interface LoadingErrorState<ERROR> {
val isLoading: Boolean
val error: ERROR?
}

private data class ContentKey<ERROR>(
override val isLoading: Boolean,
override val error: ERROR?,
) : LoadingErrorState<ERROR>

/**
* Helper that can be use as `state` argument for [ContentLoadingErrorView] when none of the composable function
* parameters need access to any state.
*/
sealed class ContentLoadingErrorState private constructor(
override val isLoading: Boolean,
override val error: Unit?,
) : LoadingErrorState<Unit> {
data object Loading : ContentLoadingErrorState(isLoading = true, error = null)
data object Content : ContentLoadingErrorState(isLoading = false, error = null)
data object Error : ContentLoadingErrorState(isLoading = false, error = Unit)
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import app.k9mail.core.ui.compose.designsystem.molecule.ContentLoadingErrorView
import app.k9mail.core.ui.compose.designsystem.molecule.ErrorView
import app.k9mail.core.ui.compose.designsystem.molecule.LoadingView
import app.k9mail.core.ui.compose.designsystem.template.ResponsiveWidthContainer
import app.k9mail.feature.account.common.ui.loadingerror.rememberContentLoadingErrorViewState
import app.k9mail.feature.account.edit.R

@Composable
Expand All @@ -27,7 +26,7 @@ fun SaveServerSettingsContent(
.then(modifier),
) {
ContentLoadingErrorView(
state = rememberContentLoadingErrorViewState(state),
state = state,
loading = {
LoadingView(
message = stringResource(id = R.string.account_edit_save_server_settings_loading_message),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package app.k9mail.feature.account.edit.ui.server.settings.save

import app.k9mail.core.ui.compose.common.mvi.UnidirectionalViewModel
import app.k9mail.feature.account.common.ui.loadingerror.LoadingErrorState
import app.k9mail.core.ui.compose.designsystem.molecule.LoadingErrorState

interface SaveServerSettingsContract {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,6 @@ class ModifyIncomingServerSettingsViewModelTest {
imapPrefix = StringInputField(value = ""),
imapUseCompression = true,
imapSendClientInfo = true,

isLoading = false,
),
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,6 @@ class ModifyOutgoingServerSettingsViewModelTest {
authenticationType = AuthenticationType.PasswordCleartext,
username = StringInputField(value = "username"),
password = StringInputField(value = "password"),

isLoading = false,
),
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,9 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
import app.k9mail.core.ui.compose.designsystem.molecule.ContentLoadingErrorView
import app.k9mail.core.ui.compose.designsystem.template.ResponsiveWidthContainer
import app.k9mail.core.ui.compose.theme2.MainTheme
import app.k9mail.feature.account.common.domain.entity.InteractionMode
import app.k9mail.feature.account.common.ui.loadingerror.rememberContentLoadingErrorViewState
import app.k9mail.feature.account.server.settings.ui.incoming.IncomingServerSettingsContract.Event
import app.k9mail.feature.account.server.settings.ui.incoming.IncomingServerSettingsContract.State
import app.k9mail.feature.account.server.settings.ui.incoming.content.incomingFormItems
Expand All @@ -38,25 +36,19 @@ internal fun IncomingServerSettingsContent(
.fillMaxWidth()
.then(modifier),
) {
ContentLoadingErrorView(
state = rememberContentLoadingErrorViewState(state = state),
loading = { /* no-op */ },
error = { /* no-op */ },
LazyColumn(
modifier = Modifier
.fillMaxSize()
.imePadding(),
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(MainTheme.spacings.default),
) {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.imePadding(),
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(MainTheme.spacings.default),
) {
incomingFormItems(
mode = mode,
state = state,
onEvent = onEvent,
resources = resources,
)
}
incomingFormItems(
mode = mode,
state = state,
onEvent = onEvent,
resources = resources,
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import app.k9mail.feature.account.common.domain.entity.toDefaultPort
import app.k9mail.feature.account.common.domain.input.NumberInputField
import app.k9mail.feature.account.common.domain.input.StringInputField
import app.k9mail.feature.account.common.ui.WithInteractionMode
import app.k9mail.feature.account.common.ui.loadingerror.LoadingErrorState

interface IncomingServerSettingsContract {

Expand All @@ -30,10 +29,7 @@ interface IncomingServerSettingsContract {
val imapPrefix: StringInputField = StringInputField(),
val imapUseCompression: Boolean = true,
val imapSendClientInfo: Boolean = true,

override val isLoading: Boolean = true,
override val error: Error? = null,
) : LoadingErrorState<Error>
)

sealed interface Event {
data class ProtocolTypeChanged(val protocolType: IncomingProtocolType) : Event
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import com.fsck.k9.mail.store.imap.ImapStoreSettings.pathPrefix
fun AccountState.toIncomingServerSettingsState() = incomingServerSettings?.toIncomingServerSettingsState()
?: State(
username = StringInputField(value = emailAddress ?: ""),
isLoading = false,
)

private fun ServerSettings.toIncomingServerSettingsState(): State {
Expand All @@ -36,9 +35,6 @@ private fun ServerSettings.toIncomingServerSettingsState(): State {
imapPrefix = StringInputField(value = pathPrefix ?: ""),
imapUseCompression = isUseCompression,
imapSendClientInfo = isSendClientInfo,

isLoading = false,
error = null,
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,9 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
import app.k9mail.core.ui.compose.designsystem.molecule.ContentLoadingErrorView
import app.k9mail.core.ui.compose.designsystem.template.ResponsiveWidthContainer
import app.k9mail.core.ui.compose.theme2.MainTheme
import app.k9mail.feature.account.common.domain.entity.InteractionMode
import app.k9mail.feature.account.common.ui.loadingerror.rememberContentLoadingErrorViewState
import app.k9mail.feature.account.server.settings.ui.outgoing.OutgoingServerSettingsContract.Event
import app.k9mail.feature.account.server.settings.ui.outgoing.OutgoingServerSettingsContract.State
import app.k9mail.feature.account.server.settings.ui.outgoing.content.outgoingFormItems
Expand All @@ -38,25 +36,19 @@ internal fun OutgoingServerSettingsContent(
.fillMaxWidth()
.then(modifier),
) {
ContentLoadingErrorView(
state = rememberContentLoadingErrorViewState(state = state),
loading = { /* no-op */ },
error = { /* no-op */ },
LazyColumn(
modifier = Modifier
.fillMaxSize()
.imePadding(),
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(MainTheme.spacings.default),
) {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.imePadding(),
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(MainTheme.spacings.default),
) {
outgoingFormItems(
mode = mode,
state = state,
onEvent = onEvent,
resources = resources,
)
}
outgoingFormItems(
mode = mode,
state = state,
onEvent = onEvent,
resources = resources,
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import app.k9mail.feature.account.common.domain.entity.toSmtpDefaultPort
import app.k9mail.feature.account.common.domain.input.NumberInputField
import app.k9mail.feature.account.common.domain.input.StringInputField
import app.k9mail.feature.account.common.ui.WithInteractionMode
import app.k9mail.feature.account.common.ui.loadingerror.LoadingErrorState

interface OutgoingServerSettingsContract {

Expand All @@ -22,10 +21,7 @@ interface OutgoingServerSettingsContract {
val username: StringInputField = StringInputField(),
val password: StringInputField = StringInputField(),
val clientCertificateAlias: String? = null,

override val isLoading: Boolean = true,
override val error: Error? = null,
) : LoadingErrorState<Error>
)

sealed interface Event {
data class ServerChanged(val server: String) : Event
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@ fun AccountState.toOutgoingServerSettingsState(): State {
?: State(
username = StringInputField(value = emailAddress ?: ""),
password = StringInputField(value = password),

isLoading = false,
)
}

Expand All @@ -38,9 +36,6 @@ private fun ServerSettings.toOutgoingServerSettingsState(password: String): Stat
authenticationType = authenticationType.toAuthenticationType(),
username = StringInputField(value = username),
password = StringInputField(value = password),

isLoading = false,
error = null,
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ class IncomingServerSettingsStateMapperKtTest {
assertThat(result).isEqualTo(
State(
username = StringInputField(value = "[email protected]"),
isLoading = false,
),
)
}
Expand All @@ -42,7 +41,7 @@ class IncomingServerSettingsStateMapperKtTest {

val result = serverSettings.toIncomingServerSettingsState()

assertThat(result).isEqualTo(INCOMING_IMAP_STATE.copy(isLoading = false))
assertThat(result).isEqualTo(INCOMING_IMAP_STATE)
}

@Test
Expand All @@ -67,7 +66,7 @@ class IncomingServerSettingsStateMapperKtTest {

val result = serverSettings.toIncomingServerSettingsState()

assertThat(result).isEqualTo(INCOMING_POP3_STATE.copy(isLoading = false))
assertThat(result).isEqualTo(INCOMING_POP3_STATE)
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,6 @@ class IncomingServerSettingsStateTest {
imapAutodetectNamespaceEnabled = true,
imapUseCompression = true,
imapSendClientInfo = true,

isLoading = true,
error = null,
),
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,6 @@ class IncomingServerSettingsViewModelTest {
imapPrefix = StringInputField(value = ""),
imapUseCompression = true,
imapSendClientInfo = true,

isLoading = false,
),
)
}
Expand Down
Loading