Validate server on user click.
This commit is contained in:
parent
48a95c6e01
commit
9a07b72a0e
21 changed files with 353 additions and 51 deletions
|
|
@ -33,7 +33,6 @@ import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.anvilannotations.ContributesNode
|
import io.element.android.anvilannotations.ContributesNode
|
||||||
import io.element.android.features.login.impl.accountprovider.AccountProviderNode
|
import io.element.android.features.login.impl.accountprovider.AccountProviderNode
|
||||||
import io.element.android.features.login.impl.accountprovider.item.AccountProvider
|
|
||||||
import io.element.android.features.login.impl.changeaccountprovider.ChangeAccountProviderNode
|
import io.element.android.features.login.impl.changeaccountprovider.ChangeAccountProviderNode
|
||||||
import io.element.android.features.login.impl.changeaccountprovider.form.ChangeAccountProviderFormNode
|
import io.element.android.features.login.impl.changeaccountprovider.form.ChangeAccountProviderFormNode
|
||||||
import io.element.android.features.login.impl.datasource.AccountProviderDataSource
|
import io.element.android.features.login.impl.datasource.AccountProviderDataSource
|
||||||
|
|
@ -125,8 +124,7 @@ class LoginFlowNode @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
NavTarget.ChangeAccountProvider -> {
|
NavTarget.ChangeAccountProvider -> {
|
||||||
val callback = object : ChangeAccountProviderNode.Callback {
|
val callback = object : ChangeAccountProviderNode.Callback {
|
||||||
override fun onAccountProviderClicked(data: AccountProvider) {
|
override fun onDone() {
|
||||||
accountProviderDataSource.userSelection(data)
|
|
||||||
// Go back to the Account Provider screen
|
// Go back to the Account Provider screen
|
||||||
backstack.singleTop(NavTarget.AccountProvider)
|
backstack.singleTop(NavTarget.AccountProvider)
|
||||||
}
|
}
|
||||||
|
|
@ -140,8 +138,7 @@ class LoginFlowNode @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
NavTarget.ChangeAccountProviderForm -> {
|
NavTarget.ChangeAccountProviderForm -> {
|
||||||
val callback = object : ChangeAccountProviderFormNode.Callback {
|
val callback = object : ChangeAccountProviderFormNode.Callback {
|
||||||
override fun onAccountProviderClicked(data: AccountProvider) {
|
override fun onDone() {
|
||||||
accountProviderDataSource.userSelection(data)
|
|
||||||
// Go back to the Account Provider screen
|
// Go back to the Account Provider screen
|
||||||
backstack.singleTop(NavTarget.AccountProvider)
|
backstack.singleTop(NavTarget.AccountProvider)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,6 @@
|
||||||
|
|
||||||
package io.element.android.features.login.impl.accountprovider
|
package io.element.android.features.login.impl.accountprovider
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
|
@ -29,10 +26,9 @@ import com.bumble.appyx.core.plugin.plugins
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.anvilannotations.ContributesNode
|
import io.element.android.anvilannotations.ContributesNode
|
||||||
import io.element.android.features.login.impl.util.LoginConstants
|
import io.element.android.features.login.impl.util.openLearnMorePage
|
||||||
import io.element.android.libraries.architecture.NodeInputs
|
import io.element.android.libraries.architecture.NodeInputs
|
||||||
import io.element.android.libraries.architecture.inputs
|
import io.element.android.libraries.architecture.inputs
|
||||||
import io.element.android.libraries.core.data.tryOrNull
|
|
||||||
import io.element.android.libraries.di.AppScope
|
import io.element.android.libraries.di.AppScope
|
||||||
import io.element.android.libraries.matrix.api.auth.OidcDetails
|
import io.element.android.libraries.matrix.api.auth.OidcDetails
|
||||||
|
|
||||||
|
|
@ -72,11 +68,6 @@ class AccountProviderNode @AssistedInject constructor(
|
||||||
plugins<Callback>().forEach { it.onChangeAccountProvider() }
|
plugins<Callback>().forEach { it.onChangeAccountProvider() }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun openLearnMorePage(context: Context) {
|
|
||||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(LoginConstants.SLIDING_SYNC_READ_MORE_URL))
|
|
||||||
tryOrNull { context.startActivity(intent) }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun View(modifier: Modifier) {
|
override fun View(modifier: Modifier) {
|
||||||
val state = presenter.present()
|
val state = presenter.present()
|
||||||
|
|
|
||||||
|
|
@ -60,8 +60,6 @@ fun AccountProviderView(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val eventSink = state.eventSink
|
val eventSink = state.eventSink
|
||||||
val invalidHomeserverError = (state.loginFlow as? Async.Failure)?.error as? ChangeServerError.InlineErrorMessage
|
|
||||||
val slidingSyncNotSupportedError = (state.loginFlow as? Async.Failure)?.error as? ChangeServerError.SlidingSyncAlert
|
|
||||||
|
|
||||||
HeaderFooterPage(
|
HeaderFooterPage(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
|
|
@ -112,7 +110,26 @@ fun AccountProviderView(
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
when (state.loginFlow) {
|
when (state.loginFlow) {
|
||||||
is Async.Failure -> Unit // Error dialog will be displayed
|
is Async.Failure -> {
|
||||||
|
when (val error = state.loginFlow.error) {
|
||||||
|
is ChangeServerError.InlineErrorMessage -> {
|
||||||
|
ErrorDialog(
|
||||||
|
content = error.message(),
|
||||||
|
onDismiss = {
|
||||||
|
eventSink.invoke(AccountProviderEvents.ClearError)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is ChangeServerError.SlidingSyncAlert -> {
|
||||||
|
SlidingSyncNotSupportedDialog(onLearnMoreClicked = {
|
||||||
|
onLearnMoreClicked()
|
||||||
|
eventSink(AccountProviderEvents.ClearError)
|
||||||
|
}, onDismiss = {
|
||||||
|
eventSink(AccountProviderEvents.ClearError)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
is Async.Loading -> Unit // The Continue button shows the loading state
|
is Async.Loading -> Unit // The Continue button shows the loading state
|
||||||
is Async.Success -> {
|
is Async.Success -> {
|
||||||
when (val loginFlowState = state.loginFlow.state) {
|
when (val loginFlowState = state.loginFlow.state) {
|
||||||
|
|
@ -122,22 +139,6 @@ fun AccountProviderView(
|
||||||
}
|
}
|
||||||
Async.Uninitialized -> Unit
|
Async.Uninitialized -> Unit
|
||||||
}
|
}
|
||||||
if (slidingSyncNotSupportedError != null) {
|
|
||||||
SlidingSyncNotSupportedDialog(onLearnMoreClicked = {
|
|
||||||
onLearnMoreClicked()
|
|
||||||
eventSink(AccountProviderEvents.ClearError)
|
|
||||||
}, onDismiss = {
|
|
||||||
eventSink(AccountProviderEvents.ClearError)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (invalidHomeserverError != null) {
|
|
||||||
ErrorDialog(
|
|
||||||
content = invalidHomeserverError.message(),
|
|
||||||
onDismiss = {
|
|
||||||
eventSink.invoke(AccountProviderEvents.ClearError)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,14 +17,20 @@
|
||||||
package io.element.android.features.login.impl.accountprovider
|
package io.element.android.features.login.impl.accountprovider
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import io.element.android.features.login.impl.R
|
import io.element.android.features.login.impl.R
|
||||||
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
|
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
|
||||||
import io.element.android.libraries.ui.strings.R as StringR
|
import io.element.android.libraries.ui.strings.R as StringR
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun SlidingSyncNotSupportedDialog(onLearnMoreClicked: () -> Unit, onDismiss: () -> Unit) {
|
internal fun SlidingSyncNotSupportedDialog(
|
||||||
|
onLearnMoreClicked: () -> Unit,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
ConfirmationDialog(
|
ConfirmationDialog(
|
||||||
|
modifier = modifier,
|
||||||
onDismiss = onDismiss,
|
onDismiss = onDismiss,
|
||||||
submitText = stringResource(StringR.string.action_learn_more),
|
submitText = stringResource(StringR.string.action_learn_more),
|
||||||
onSubmitClicked = onLearnMoreClicked,
|
onSubmitClicked = onLearnMoreClicked,
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ package io.element.android.features.login.impl.changeaccountprovider
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import com.bumble.appyx.core.modality.BuildContext
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
import com.bumble.appyx.core.node.Node
|
import com.bumble.appyx.core.node.Node
|
||||||
import com.bumble.appyx.core.plugin.Plugin
|
import com.bumble.appyx.core.plugin.Plugin
|
||||||
|
|
@ -25,7 +26,7 @@ import com.bumble.appyx.core.plugin.plugins
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.anvilannotations.ContributesNode
|
import io.element.android.anvilannotations.ContributesNode
|
||||||
import io.element.android.features.login.impl.accountprovider.item.AccountProvider
|
import io.element.android.features.login.impl.util.openLearnMorePage
|
||||||
import io.element.android.libraries.di.AppScope
|
import io.element.android.libraries.di.AppScope
|
||||||
|
|
||||||
@ContributesNode(AppScope::class)
|
@ContributesNode(AppScope::class)
|
||||||
|
|
@ -36,12 +37,12 @@ class ChangeAccountProviderNode @AssistedInject constructor(
|
||||||
) : Node(buildContext, plugins = plugins) {
|
) : Node(buildContext, plugins = plugins) {
|
||||||
|
|
||||||
interface Callback : Plugin {
|
interface Callback : Plugin {
|
||||||
fun onAccountProviderClicked(data: AccountProvider)
|
fun onDone()
|
||||||
fun onOtherClicked()
|
fun onOtherClicked()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onAccountProviderClicked(data: AccountProvider) {
|
private fun onDone() {
|
||||||
plugins<Callback>().forEach { it.onAccountProviderClicked(data) }
|
plugins<Callback>().forEach { it.onDone() }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onOtherClicked() {
|
private fun onOtherClicked() {
|
||||||
|
|
@ -51,11 +52,13 @@ class ChangeAccountProviderNode @AssistedInject constructor(
|
||||||
@Composable
|
@Composable
|
||||||
override fun View(modifier: Modifier) {
|
override fun View(modifier: Modifier) {
|
||||||
val state = presenter.present()
|
val state = presenter.present()
|
||||||
|
val context = LocalContext.current
|
||||||
ChangeAccountProviderView(
|
ChangeAccountProviderView(
|
||||||
state = state,
|
state = state,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
onBackPressed = ::navigateUp,
|
onBackPressed = ::navigateUp,
|
||||||
onAccountProviderClicked = ::onAccountProviderClicked,
|
onLearnMoreClicked = { openLearnMorePage(context) },
|
||||||
|
onDone = ::onDone,
|
||||||
onOtherProviderClicked = ::onOtherClicked,
|
onOtherProviderClicked = ::onOtherClicked,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,14 +18,17 @@ package io.element.android.features.login.impl.changeaccountprovider
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import io.element.android.features.login.impl.accountprovider.item.AccountProvider
|
import io.element.android.features.login.impl.accountprovider.item.AccountProvider
|
||||||
|
import io.element.android.features.login.impl.changeaccountprovider.common.ChangeServerPresenter
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class ChangeAccountProviderPresenter @Inject constructor(
|
class ChangeAccountProviderPresenter @Inject constructor(
|
||||||
|
private val changeServerPresenter: ChangeServerPresenter,
|
||||||
) : Presenter<ChangeAccountProviderState> {
|
) : Presenter<ChangeAccountProviderState> {
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): ChangeAccountProviderState {
|
override fun present(): ChangeAccountProviderState {
|
||||||
|
val changeServerState = changeServerPresenter.present()
|
||||||
return ChangeAccountProviderState(
|
return ChangeAccountProviderState(
|
||||||
// Just matrix.org by default for now
|
// Just matrix.org by default for now
|
||||||
accountProviders = listOf(
|
accountProviders = listOf(
|
||||||
|
|
@ -38,6 +41,7 @@ class ChangeAccountProviderPresenter @Inject constructor(
|
||||||
supportSlidingSync = true,
|
supportSlidingSync = true,
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
changeServerState = changeServerState,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,10 @@
|
||||||
package io.element.android.features.login.impl.changeaccountprovider
|
package io.element.android.features.login.impl.changeaccountprovider
|
||||||
|
|
||||||
import io.element.android.features.login.impl.accountprovider.item.AccountProvider
|
import io.element.android.features.login.impl.accountprovider.item.AccountProvider
|
||||||
|
import io.element.android.features.login.impl.changeaccountprovider.common.ChangeServerState
|
||||||
|
|
||||||
// Do not use default value, so no member get forgotten in the presenters.
|
// Do not use default value, so no member get forgotten in the presenters.
|
||||||
data class ChangeAccountProviderState constructor(
|
data class ChangeAccountProviderState constructor(
|
||||||
val accountProviders: List<AccountProvider>,
|
val accountProviders: List<AccountProvider>,
|
||||||
|
val changeServerState: ChangeServerState,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ package io.element.android.features.login.impl.changeaccountprovider
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.features.login.impl.accountprovider.item.anAccountProvider
|
import io.element.android.features.login.impl.accountprovider.item.anAccountProvider
|
||||||
|
import io.element.android.features.login.impl.changeaccountprovider.common.aChangeServerState
|
||||||
|
|
||||||
open class ChangeAccountProviderStateProvider : PreviewParameterProvider<ChangeAccountProviderState> {
|
open class ChangeAccountProviderStateProvider : PreviewParameterProvider<ChangeAccountProviderState> {
|
||||||
override val values: Sequence<ChangeAccountProviderState>
|
override val values: Sequence<ChangeAccountProviderState>
|
||||||
|
|
@ -31,4 +32,5 @@ fun aChangeAccountProviderState() = ChangeAccountProviderState(
|
||||||
accountProviders = listOf(
|
accountProviders = listOf(
|
||||||
anAccountProvider()
|
anAccountProvider()
|
||||||
),
|
),
|
||||||
|
changeServerState = aChangeServerState(),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,8 @@ import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.features.login.impl.R
|
import io.element.android.features.login.impl.R
|
||||||
import io.element.android.features.login.impl.accountprovider.item.AccountProvider
|
import io.element.android.features.login.impl.accountprovider.item.AccountProvider
|
||||||
import io.element.android.features.login.impl.accountprovider.item.AccountProviderView
|
import io.element.android.features.login.impl.accountprovider.item.AccountProviderView
|
||||||
|
import io.element.android.features.login.impl.changeaccountprovider.common.ChangeServerEvents
|
||||||
|
import io.element.android.features.login.impl.changeaccountprovider.common.ChangeServerView
|
||||||
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
|
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
|
||||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
|
|
@ -57,7 +59,8 @@ fun ChangeAccountProviderView(
|
||||||
state: ChangeAccountProviderState,
|
state: ChangeAccountProviderState,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onBackPressed: () -> Unit = {},
|
onBackPressed: () -> Unit = {},
|
||||||
onAccountProviderClicked: (AccountProvider) -> Unit = {},
|
onLearnMoreClicked: () -> Unit = {},
|
||||||
|
onDone: () -> Unit = {},
|
||||||
onOtherProviderClicked: () -> Unit = {},
|
onOtherProviderClicked: () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
val scrollState = rememberScrollState()
|
val scrollState = rememberScrollState()
|
||||||
|
|
@ -104,7 +107,7 @@ fun ChangeAccountProviderView(
|
||||||
AccountProviderView(
|
AccountProviderView(
|
||||||
item = alteredItem,
|
item = alteredItem,
|
||||||
onClick = {
|
onClick = {
|
||||||
onAccountProviderClicked(alteredItem)
|
state.changeServerState.eventSink.invoke(ChangeServerEvents.ChangeServer(alteredItem))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -117,6 +120,11 @@ fun ChangeAccountProviderView(
|
||||||
)
|
)
|
||||||
Spacer(Modifier.height(32.dp))
|
Spacer(Modifier.height(32.dp))
|
||||||
}
|
}
|
||||||
|
ChangeServerView(
|
||||||
|
state = state.changeServerState,
|
||||||
|
onLearnMoreClicked = onLearnMoreClicked,
|
||||||
|
onDone = onDone,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.login.impl.changeaccountprovider.common
|
||||||
|
|
||||||
|
import io.element.android.features.login.impl.accountprovider.item.AccountProvider
|
||||||
|
|
||||||
|
sealed interface ChangeServerEvents {
|
||||||
|
data class ChangeServer(val accountProvider: AccountProvider) : ChangeServerEvents
|
||||||
|
object ClearError : ChangeServerEvents
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,76 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.login.impl.changeaccountprovider.common
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.MutableState
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import io.element.android.features.login.impl.accountprovider.item.AccountProvider
|
||||||
|
import io.element.android.features.login.impl.datasource.AccountProviderDataSource
|
||||||
|
import io.element.android.features.login.impl.error.ChangeServerError
|
||||||
|
import io.element.android.libraries.architecture.Async
|
||||||
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
import io.element.android.libraries.architecture.execute
|
||||||
|
import io.element.android.libraries.core.data.tryOrNull
|
||||||
|
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.net.URL
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class ChangeServerPresenter @Inject constructor(
|
||||||
|
private val authenticationService: MatrixAuthenticationService,
|
||||||
|
private val accountProviderDataSource: AccountProviderDataSource,
|
||||||
|
) : Presenter<ChangeServerState> {
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun present(): ChangeServerState {
|
||||||
|
val localCoroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
val changeServerAction: MutableState<Async<Unit>> = remember {
|
||||||
|
mutableStateOf(Async.Uninitialized)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun handleEvents(event: ChangeServerEvents) {
|
||||||
|
when (event) {
|
||||||
|
is ChangeServerEvents.ChangeServer -> localCoroutineScope.changeServer(event.accountProvider, changeServerAction)
|
||||||
|
ChangeServerEvents.ClearError -> changeServerAction.value = Async.Uninitialized
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ChangeServerState(
|
||||||
|
changeServerAction = changeServerAction.value,
|
||||||
|
eventSink = ::handleEvents
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun CoroutineScope.changeServer(
|
||||||
|
data: AccountProvider,
|
||||||
|
changeServerAction: MutableState<Async<Unit>>,
|
||||||
|
) = launch {
|
||||||
|
suspend {
|
||||||
|
val domain = tryOrNull { URL(data.title) }?.host ?: data.title
|
||||||
|
authenticationService.setHomeserver(domain).map {
|
||||||
|
authenticationService.getHomeserverDetails().value!!
|
||||||
|
// Valid, remember user choice
|
||||||
|
accountProviderDataSource.userSelection(data)
|
||||||
|
}.getOrThrow()
|
||||||
|
}.execute(changeServerAction, errorMapping = ChangeServerError::from)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.login.impl.changeaccountprovider.common
|
||||||
|
|
||||||
|
import io.element.android.libraries.architecture.Async
|
||||||
|
|
||||||
|
data class ChangeServerState(
|
||||||
|
val changeServerAction: Async<Unit>,
|
||||||
|
val eventSink: (ChangeServerEvents) -> Unit
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.login.impl.changeaccountprovider.common
|
||||||
|
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
|
import io.element.android.libraries.architecture.Async
|
||||||
|
|
||||||
|
open class ChangeServerStateProvider : PreviewParameterProvider<ChangeServerState> {
|
||||||
|
override val values: Sequence<ChangeServerState>
|
||||||
|
get() = sequenceOf(
|
||||||
|
aChangeServerState(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun aChangeServerState() = ChangeServerState(
|
||||||
|
changeServerAction = Async.Uninitialized,
|
||||||
|
eventSink = {}
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.login.impl.changeaccountprovider.common
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
import io.element.android.features.login.impl.accountprovider.SlidingSyncNotSupportedDialog
|
||||||
|
import io.element.android.features.login.impl.error.ChangeServerError
|
||||||
|
import io.element.android.libraries.architecture.Async
|
||||||
|
import io.element.android.libraries.designsystem.components.ProgressDialog
|
||||||
|
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
|
||||||
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
|
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ChangeServerView(
|
||||||
|
state: ChangeServerState,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onLearnMoreClicked: () -> Unit = {},
|
||||||
|
onDone: () -> Unit = {},
|
||||||
|
) {
|
||||||
|
val eventSink = state.eventSink
|
||||||
|
when (state.changeServerAction) {
|
||||||
|
is Async.Failure -> {
|
||||||
|
when (val error = state.changeServerAction.error) {
|
||||||
|
is ChangeServerError.InlineErrorMessage -> {
|
||||||
|
ErrorDialog(
|
||||||
|
modifier = modifier,
|
||||||
|
content = error.message(),
|
||||||
|
onDismiss = {
|
||||||
|
eventSink.invoke(ChangeServerEvents.ClearError)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is ChangeServerError.SlidingSyncAlert -> {
|
||||||
|
SlidingSyncNotSupportedDialog(
|
||||||
|
modifier = modifier,
|
||||||
|
onLearnMoreClicked = {
|
||||||
|
onLearnMoreClicked()
|
||||||
|
eventSink.invoke(ChangeServerEvents.ClearError)
|
||||||
|
}, onDismiss = {
|
||||||
|
eventSink.invoke(ChangeServerEvents.ClearError)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is Async.Loading -> ProgressDialog()
|
||||||
|
is Async.Success -> onDone()
|
||||||
|
Async.Uninitialized -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun ChangeServerViewLightPreview(@PreviewParameter(ChangeServerStateProvider::class) state: ChangeServerState) =
|
||||||
|
ElementPreviewLight { ContentToPreview(state) }
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun ChangeServerViewDarkPreview(@PreviewParameter(ChangeServerStateProvider::class) state: ChangeServerState) =
|
||||||
|
ElementPreviewDark { ContentToPreview(state) }
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ContentToPreview(state: ChangeServerState) {
|
||||||
|
ChangeServerView(
|
||||||
|
state = state,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -18,6 +18,7 @@ package io.element.android.features.login.impl.changeaccountprovider.form
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import com.bumble.appyx.core.modality.BuildContext
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
import com.bumble.appyx.core.node.Node
|
import com.bumble.appyx.core.node.Node
|
||||||
import com.bumble.appyx.core.plugin.Plugin
|
import com.bumble.appyx.core.plugin.Plugin
|
||||||
|
|
@ -25,7 +26,7 @@ import com.bumble.appyx.core.plugin.plugins
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.anvilannotations.ContributesNode
|
import io.element.android.anvilannotations.ContributesNode
|
||||||
import io.element.android.features.login.impl.accountprovider.item.AccountProvider
|
import io.element.android.features.login.impl.util.openLearnMorePage
|
||||||
import io.element.android.libraries.di.AppScope
|
import io.element.android.libraries.di.AppScope
|
||||||
|
|
||||||
@ContributesNode(AppScope::class)
|
@ContributesNode(AppScope::class)
|
||||||
|
|
@ -36,21 +37,23 @@ class ChangeAccountProviderFormNode @AssistedInject constructor(
|
||||||
) : Node(buildContext, plugins = plugins) {
|
) : Node(buildContext, plugins = plugins) {
|
||||||
|
|
||||||
interface Callback : Plugin {
|
interface Callback : Plugin {
|
||||||
fun onAccountProviderClicked(data: AccountProvider)
|
fun onDone()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onAccountProviderClicked(data: AccountProvider) {
|
private fun onDone() {
|
||||||
plugins<Callback>().forEach { it.onAccountProviderClicked(data) }
|
plugins<Callback>().forEach { it.onDone() }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun View(modifier: Modifier) {
|
override fun View(modifier: Modifier) {
|
||||||
val state = presenter.present()
|
val state = presenter.present()
|
||||||
|
val context = LocalContext.current
|
||||||
ChangeAccountProviderFormView(
|
ChangeAccountProviderFormView(
|
||||||
state = state,
|
state = state,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
onBackPressed = ::navigateUp,
|
onBackPressed = ::navigateUp,
|
||||||
onAccountProviderClicked = ::onAccountProviderClicked
|
onLearnMoreClicked = { openLearnMorePage(context) },
|
||||||
|
onDone = ::onDone,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import io.element.android.features.login.impl.changeaccountprovider.common.ChangeServerPresenter
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
@ -29,6 +30,7 @@ import javax.inject.Inject
|
||||||
|
|
||||||
class ChangeAccountProviderFormPresenter @Inject constructor(
|
class ChangeAccountProviderFormPresenter @Inject constructor(
|
||||||
private val homeserverResolver: HomeserverResolver,
|
private val homeserverResolver: HomeserverResolver,
|
||||||
|
private val changeServerPresenter: ChangeServerPresenter,
|
||||||
) : Presenter<ChangeAccountProviderFormState> {
|
) : Presenter<ChangeAccountProviderFormState> {
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -38,6 +40,7 @@ class ChangeAccountProviderFormPresenter @Inject constructor(
|
||||||
val userInput = rememberSaveable {
|
val userInput = rememberSaveable {
|
||||||
mutableStateOf("")
|
mutableStateOf("")
|
||||||
}
|
}
|
||||||
|
val changeServerState = changeServerPresenter.present()
|
||||||
val data by homeserverResolver.flow().collectAsState()
|
val data by homeserverResolver.flow().collectAsState()
|
||||||
|
|
||||||
fun handleEvents(event: ChangeAccountProviderFormEvents) {
|
fun handleEvents(event: ChangeAccountProviderFormEvents) {
|
||||||
|
|
@ -52,6 +55,7 @@ class ChangeAccountProviderFormPresenter @Inject constructor(
|
||||||
return ChangeAccountProviderFormState(
|
return ChangeAccountProviderFormState(
|
||||||
userInput = userInput.value,
|
userInput = userInput.value,
|
||||||
userInputResult = data,
|
userInputResult = data,
|
||||||
|
changeServerState = changeServerState,
|
||||||
eventSink = ::handleEvents
|
eventSink = ::handleEvents
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,11 +16,13 @@
|
||||||
|
|
||||||
package io.element.android.features.login.impl.changeaccountprovider.form
|
package io.element.android.features.login.impl.changeaccountprovider.form
|
||||||
|
|
||||||
|
import io.element.android.features.login.impl.changeaccountprovider.common.ChangeServerState
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
|
|
||||||
// Do not use default value, so no member get forgotten in the presenters.
|
// Do not use default value, so no member get forgotten in the presenters.
|
||||||
data class ChangeAccountProviderFormState(
|
data class ChangeAccountProviderFormState(
|
||||||
val userInput: String,
|
val userInput: String,
|
||||||
val userInputResult: Async<List<HomeserverData>>,
|
val userInputResult: Async<List<HomeserverData>>,
|
||||||
|
val changeServerState: ChangeServerState,
|
||||||
val eventSink: (ChangeAccountProviderFormEvents) -> Unit
|
val eventSink: (ChangeAccountProviderFormEvents) -> Unit
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
package io.element.android.features.login.impl.changeaccountprovider.form
|
package io.element.android.features.login.impl.changeaccountprovider.form
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
|
import io.element.android.features.login.impl.changeaccountprovider.common.aChangeServerState
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
|
|
||||||
open class ChangeAccountProviderFormStateProvider : PreviewParameterProvider<ChangeAccountProviderFormState> {
|
open class ChangeAccountProviderFormStateProvider : PreviewParameterProvider<ChangeAccountProviderFormState> {
|
||||||
|
|
@ -34,6 +35,7 @@ fun aChangeAccountProviderFormState(
|
||||||
) = ChangeAccountProviderFormState(
|
) = ChangeAccountProviderFormState(
|
||||||
userInput = userInput,
|
userInput = userInput,
|
||||||
userInputResult = userInputResult,
|
userInputResult = userInputResult,
|
||||||
|
changeServerState = aChangeServerState(),
|
||||||
eventSink = {}
|
eventSink = {}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,8 @@ import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.features.login.impl.R
|
import io.element.android.features.login.impl.R
|
||||||
import io.element.android.features.login.impl.accountprovider.item.AccountProvider
|
import io.element.android.features.login.impl.accountprovider.item.AccountProvider
|
||||||
import io.element.android.features.login.impl.accountprovider.item.AccountProviderView
|
import io.element.android.features.login.impl.accountprovider.item.AccountProviderView
|
||||||
|
import io.element.android.features.login.impl.changeaccountprovider.common.ChangeServerEvents
|
||||||
|
import io.element.android.features.login.impl.changeaccountprovider.common.ChangeServerView
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
|
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
|
||||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||||
|
|
@ -72,7 +74,8 @@ fun ChangeAccountProviderFormView(
|
||||||
state: ChangeAccountProviderFormState,
|
state: ChangeAccountProviderFormState,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onBackPressed: () -> Unit = {},
|
onBackPressed: () -> Unit = {},
|
||||||
onAccountProviderClicked: (AccountProvider) -> Unit = {},
|
onLearnMoreClicked: () -> Unit = {},
|
||||||
|
onDone: () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
val eventSink = state.eventSink
|
val eventSink = state.eventSink
|
||||||
val scrollState = rememberScrollState()
|
val scrollState = rememberScrollState()
|
||||||
|
|
@ -164,7 +167,7 @@ fun ChangeAccountProviderFormView(
|
||||||
AccountProviderView(
|
AccountProviderView(
|
||||||
item = item,
|
item = item,
|
||||||
onClick = {
|
onClick = {
|
||||||
onAccountProviderClicked(item)
|
state.changeServerState.eventSink.invoke(ChangeServerEvents.ChangeServer(item))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -173,6 +176,11 @@ fun ChangeAccountProviderFormView(
|
||||||
}
|
}
|
||||||
Spacer(Modifier.height(32.dp))
|
Spacer(Modifier.height(32.dp))
|
||||||
}
|
}
|
||||||
|
ChangeServerView(
|
||||||
|
state = state.changeServerState,
|
||||||
|
onLearnMoreClicked = onLearnMoreClicked,
|
||||||
|
onDone = onDone,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.login.impl.util
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import io.element.android.libraries.core.data.tryOrNull
|
||||||
|
|
||||||
|
fun openLearnMorePage(context: Context) {
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(LoginConstants.SLIDING_SYNC_READ_MORE_URL))
|
||||||
|
tryOrNull { context.startActivity(intent) }
|
||||||
|
}
|
||||||
|
|
@ -24,12 +24,14 @@ import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
import io.element.android.libraries.designsystem.theme.components.Button
|
import io.element.android.libraries.designsystem.theme.components.Button
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
|
import io.element.android.libraries.ui.strings.R as StringR
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AsyncFailure(
|
fun AsyncFailure(
|
||||||
|
|
@ -43,11 +45,11 @@ fun AsyncFailure(
|
||||||
.padding(vertical = 32.dp),
|
.padding(vertical = 32.dp),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
) {
|
) {
|
||||||
Text(text = throwable.message ?: "An error occurred")
|
Text(text = throwable.message ?: stringResource(id = StringR.string.error_unknown))
|
||||||
if (onRetry != null) {
|
if (onRetry != null) {
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
Button(onClick = onRetry) {
|
Button(onClick = onRetry) {
|
||||||
Text(text = "Retry")
|
Text(text = stringResource(id = StringR.string.action_retry))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue