Browse Source

Navigation

feature/fga/small_timeline_improvements
Benoit Marty 1 year ago committed by Benoit Marty
parent
commit
974ec9c1f7
  1. 17
      features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt
  2. 4
      features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderEvents.kt
  3. 25
      features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderNode.kt
  4. 48
      features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderPresenter.kt
  5. 8
      features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderState.kt
  6. 2
      features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderStateProvider.kt
  7. 60
      features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderView.kt
  8. 42
      features/login/impl/src/main/kotlin/io/element/android/features/login/impl/datasource/AccountProviderDataSource.kt
  9. 10
      features/login/impl/src/main/kotlin/io/element/android/features/login/impl/util/LoginConstants.kt

17
features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt

@ -28,6 +28,7 @@ import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.navmodel.backstack.BackStack import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.push import com.bumble.appyx.navmodel.backstack.operation.push
import com.bumble.appyx.navmodel.backstack.operation.singleTop
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
@ -36,11 +37,11 @@ import io.element.android.features.login.impl.changeaccountprovider.ChangeAccoun
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.changeaccountprovider.item.AccountProviderItem import io.element.android.features.login.impl.changeaccountprovider.item.AccountProviderItem
import io.element.android.features.login.impl.changeserver.ChangeServerNode import io.element.android.features.login.impl.changeserver.ChangeServerNode
import io.element.android.features.login.impl.datasource.AccountProviderDataSource
import io.element.android.features.login.impl.oidc.CustomTabAvailabilityChecker import io.element.android.features.login.impl.oidc.CustomTabAvailabilityChecker
import io.element.android.features.login.impl.oidc.customtab.CustomTabHandler import io.element.android.features.login.impl.oidc.customtab.CustomTabHandler
import io.element.android.features.login.impl.oidc.webview.OidcNode import io.element.android.features.login.impl.oidc.webview.OidcNode
import io.element.android.features.login.impl.root.LoginRootNode import io.element.android.features.login.impl.root.LoginRootNode
import io.element.android.features.login.impl.util.LoginConstants
import io.element.android.libraries.architecture.BackstackNode import io.element.android.libraries.architecture.BackstackNode
import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
@ -57,6 +58,7 @@ class LoginFlowNode @AssistedInject constructor(
@Assisted plugins: List<Plugin>, @Assisted plugins: List<Plugin>,
private val customTabAvailabilityChecker: CustomTabAvailabilityChecker, private val customTabAvailabilityChecker: CustomTabAvailabilityChecker,
private val customTabHandler: CustomTabHandler, private val customTabHandler: CustomTabHandler,
private val accountProviderDataSource: AccountProviderDataSource,
) : BackstackNode<LoginFlowNode.NavTarget>( ) : BackstackNode<LoginFlowNode.NavTarget>(
backstack = BackStack( backstack = BackStack(
initialElement = NavTarget.AccountProvider, // NavTarget.Root, initialElement = NavTarget.AccountProvider, // NavTarget.Root,
@ -124,12 +126,11 @@ class LoginFlowNode @AssistedInject constructor(
} }
NavTarget.AccountProvider -> { NavTarget.AccountProvider -> {
val inputs = AccountProviderNode.Inputs( val inputs = AccountProviderNode.Inputs(
homeserver = LoginConstants.DEFAULT_HOMESERVER_URL,
isMatrixOrg = LoginConstants.DEFAULT_HOMESERVER_URL == "matrix.org",
isAccountCreation = inputs.isAccountCreation isAccountCreation = inputs.isAccountCreation
) )
val callback = object : AccountProviderNode.Callback { val callback = object : AccountProviderNode.Callback {
override fun onContinue() { override fun onServerValidated() {
// TODO
TODO("Not yet implemented") TODO("Not yet implemented")
} }
@ -142,7 +143,9 @@ class LoginFlowNode @AssistedInject constructor(
NavTarget.ChangeAccountProvider -> { NavTarget.ChangeAccountProvider -> {
val callback = object : ChangeAccountProviderNode.Callback { val callback = object : ChangeAccountProviderNode.Callback {
override fun onAccountProviderItemClicked(data: AccountProviderItem) { override fun onAccountProviderItemClicked(data: AccountProviderItem) {
TODO("Not yet implemented") accountProviderDataSource.userSelection(data)
// Go back to the Account Provider screen
backstack.singleTop(NavTarget.AccountProvider)
} }
override fun onOtherClicked() { override fun onOtherClicked() {
@ -155,7 +158,9 @@ class LoginFlowNode @AssistedInject constructor(
NavTarget.ChangeAccountProviderForm -> { NavTarget.ChangeAccountProviderForm -> {
val callback = object : ChangeAccountProviderFormNode.Callback { val callback = object : ChangeAccountProviderFormNode.Callback {
override fun onAccountProviderItemClicked(data: AccountProviderItem) { override fun onAccountProviderItemClicked(data: AccountProviderItem) {
TODO("Not yet implemented") accountProviderDataSource.userSelection(data)
// Go back to the Account Provider screen
backstack.singleTop(NavTarget.AccountProvider)
} }
} }

4
features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderEvents.kt

@ -16,7 +16,7 @@
package io.element.android.features.login.impl.accountprovider package io.element.android.features.login.impl.accountprovider
// TODO Add your events or remove the file completely if no events
sealed interface AccountProviderEvents { sealed interface AccountProviderEvents {
object MyEvent : AccountProviderEvents object Continue : AccountProviderEvents
object ClearError : AccountProviderEvents
} }

25
features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderNode.kt

@ -16,8 +16,12 @@
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 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,8 +29,10 @@ 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.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
@ContributesNode(AppScope::class) @ContributesNode(AppScope::class)
@ -37,41 +43,44 @@ class AccountProviderNode @AssistedInject constructor(
) : Node(buildContext, plugins = plugins) { ) : Node(buildContext, plugins = plugins) {
data class Inputs( data class Inputs(
val homeserver: String,
val isMatrixOrg: Boolean,
val isAccountCreation: Boolean, val isAccountCreation: Boolean,
) : NodeInputs ) : NodeInputs
private val inputs: Inputs = inputs() private val inputs: Inputs = inputs()
private val presenter = presenterFactory.create( private val presenter = presenterFactory.create(
AccountProviderPresenterParams( AccountProviderPresenterParams(
homeserver = inputs.homeserver,
isMatrixOrg = inputs.isMatrixOrg,
isAccountCreation = inputs.isAccountCreation, isAccountCreation = inputs.isAccountCreation,
) )
) )
interface Callback : Plugin { interface Callback : Plugin {
fun onContinue() fun onServerValidated()
fun onChangeAccountProvider() fun onChangeAccountProvider()
} }
private fun onContinue() { private fun onServerValidated() {
plugins<Callback>().forEach { it.onContinue() } plugins<Callback>().forEach { it.onServerValidated() }
} }
private fun onChangeAccountProvider() { private fun onChangeAccountProvider() {
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()
val context = LocalContext.current
AccountProviderView( AccountProviderView(
state = state, state = state,
modifier = modifier, modifier = modifier,
onContinue = ::onContinue, onChangeServerSuccess = ::onServerValidated,
onChange = ::onChangeAccountProvider, onChange = ::onChangeAccountProvider,
onLearnMoreClicked = { openLearnMorePage(context) },
) )
} }
} }

48
features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderPresenter.kt

@ -17,19 +17,35 @@
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.runtime.MutableState
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import io.element.android.features.login.impl.changeserver.ChangeServerError
import io.element.android.features.login.impl.datasource.AccountProviderDataSource
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.architecture.Presenter 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
data class AccountProviderPresenterParams( data class AccountProviderPresenterParams(
val homeserver: String,
val isMatrixOrg: Boolean,
val isAccountCreation: Boolean, val isAccountCreation: Boolean,
) )
class AccountProviderPresenter @AssistedInject constructor( class AccountProviderPresenter @AssistedInject constructor(
@Assisted private val params: AccountProviderPresenterParams, @Assisted private val params: AccountProviderPresenterParams,
private val accountProviderDataSource: AccountProviderDataSource,
private val authenticationService: MatrixAuthenticationService
) : Presenter<AccountProviderState> { ) : Presenter<AccountProviderState> {
@AssistedFactory @AssistedFactory
@ -39,18 +55,40 @@ class AccountProviderPresenter @AssistedInject constructor(
@Composable @Composable
override fun present(): AccountProviderState { override fun present(): AccountProviderState {
val accountProvider by accountProviderDataSource.flow().collectAsState()
val localCoroutineScope = rememberCoroutineScope()
val homeserver = rememberSaveable {
mutableStateOf(accountProvider.title /* TODO There is a mix of data and UI here, which is not nice */)
}
val changeServerAction: MutableState<Async<Unit>> = remember {
mutableStateOf(Async.Uninitialized)
}
fun handleEvents(event: AccountProviderEvents) { fun handleEvents(event: AccountProviderEvents) {
when (event) { when (event) {
AccountProviderEvents.MyEvent -> Unit AccountProviderEvents.Continue -> {
localCoroutineScope.submit(homeserver, changeServerAction)
}
AccountProviderEvents.ClearError -> changeServerAction.value = Async.Uninitialized
} }
} }
return AccountProviderState( return AccountProviderState(
homeserver = params.homeserver, homeserver = accountProvider.title,
isMatrix = params.isMatrixOrg, isMatrix = accountProvider.isMatrixOrg,
isAccountCreation = params.isAccountCreation, isAccountCreation = params.isAccountCreation,
changeServerAction = changeServerAction.value,
eventSink = ::handleEvents eventSink = ::handleEvents
) )
} }
private fun CoroutineScope.submit(homeserverUrl: MutableState<String>, changeServerAction: MutableState<Async<Unit>>) = launch {
suspend {
val domain = tryOrNull { URL(homeserverUrl.value) }?.host ?: homeserverUrl.value
authenticationService.setHomeserver(domain).getOrThrow()
homeserverUrl.value = domain
}.execute(changeServerAction, errorMapping = ChangeServerError::from)
}
} }

8
features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderState.kt

@ -16,10 +16,16 @@
package io.element.android.features.login.impl.accountprovider package io.element.android.features.login.impl.accountprovider
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 AccountProviderState( data class AccountProviderState(
val homeserver: String, val homeserver: String,
val isMatrix: Boolean, val isMatrix: Boolean,
val isAccountCreation: Boolean, val isAccountCreation: Boolean,
// TODO Rename
val changeServerAction: Async<Unit>,
val eventSink: (AccountProviderEvents) -> Unit val eventSink: (AccountProviderEvents) -> Unit
) ) {
val submitEnabled: Boolean get() = homeserver.isNotEmpty() && (changeServerAction is Async.Uninitialized || changeServerAction is Async.Loading)
}

2
features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderStateProvider.kt

@ -17,6 +17,7 @@
package io.element.android.features.login.impl.accountprovider package io.element.android.features.login.impl.accountprovider
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.architecture.Async
open class AccountProviderStateProvider : PreviewParameterProvider<AccountProviderState> { open class AccountProviderStateProvider : PreviewParameterProvider<AccountProviderState> {
override val values: Sequence<AccountProviderState> override val values: Sequence<AccountProviderState>
@ -30,5 +31,6 @@ fun aAccountProviderState() = AccountProviderState(
homeserver = "matrix.org", homeserver = "matrix.org",
isMatrix = true, isMatrix = true,
isAccountCreation = false, isAccountCreation = false,
changeServerAction = Async.Uninitialized,
eventSink = {} eventSink = {}
) )

60
features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderView.kt

@ -21,28 +21,49 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AccountCircle import androidx.compose.material.icons.filled.AccountCircle
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp 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.changeserver.ChangeServerError
import io.element.android.features.login.impl.changeserver.ChangeServerEvents
import io.element.android.features.login.impl.changeserver.SlidingSyncNotSupportedDialog
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
import io.element.android.libraries.designsystem.components.button.ButtonWithProgress
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
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.Text import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TextButton import io.element.android.libraries.designsystem.theme.components.TextButton
import io.element.android.libraries.testtags.TestTags
import io.element.android.libraries.testtags.testTag
@Composable @Composable
fun AccountProviderView( fun AccountProviderView(
state: AccountProviderState, state: AccountProviderState,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onContinue: () -> Unit = {}, // TODO Rename
onChangeServerSuccess: () -> Unit = {},
onLearnMoreClicked: () -> Unit = {},
onChange: () -> Unit = {}, onChange: () -> Unit = {},
) { ) {
val isLoading by remember(state.changeServerAction) {
derivedStateOf {
state.changeServerAction is Async.Loading
}
}
val eventSink = state.eventSink
val invalidHomeserverError = (state.changeServerAction as? Async.Failure)?.error as? ChangeServerError.InlineErrorMessage
val slidingSyncNotSupportedError = (state.changeServerAction as? Async.Failure)?.error as? ChangeServerError.SlidingSyncAlert
HeaderFooterPage( HeaderFooterPage(
modifier = modifier, modifier = modifier,
header = { header = {
@ -69,16 +90,15 @@ fun AccountProviderView(
}, },
footer = { footer = {
ButtonColumnMolecule { ButtonColumnMolecule {
Button( ButtonWithProgress(
onClick = { text = stringResource(id = R.string.screen_account_provider_continue),
onContinue() showProgress = isLoading,
}, onClick = { eventSink.invoke(AccountProviderEvents.Continue) },
enabled = true, enabled = state.submitEnabled,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
) { .testTag(TestTags.changeServerContinue)
Text(text = stringResource(id = R.string.screen_account_provider_continue)) )
}
TextButton( TextButton(
onClick = { onClick = {
onChange() onChange()
@ -92,7 +112,25 @@ fun AccountProviderView(
} }
} }
) { ) {
// No content 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)
}
)
}
}
if (state.changeServerAction is Async.Success) {
onChangeServerSuccess()
} }
} }

42
features/login/impl/src/main/kotlin/io/element/android/features/login/impl/datasource/AccountProviderDataSource.kt

@ -0,0 +1,42 @@
/*
* 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.datasource
import io.element.android.features.login.impl.changeaccountprovider.item.AccountProviderItem
import io.element.android.features.login.impl.util.defaultAccountProviderItem
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.SingleIn
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import javax.inject.Inject
@SingleIn(AppScope::class)
class AccountProviderDataSource @Inject constructor(
) {
private val accountProvider: MutableStateFlow<AccountProviderItem> = MutableStateFlow(
defaultAccountProviderItem
)
fun flow(): StateFlow<AccountProviderItem> {
return accountProvider.asStateFlow()
}
fun userSelection(data: AccountProviderItem) {
accountProvider.tryEmit(data)
}
}

10
features/login/impl/src/main/kotlin/io/element/android/features/login/impl/util/LoginConstants.kt

@ -16,8 +16,18 @@
package io.element.android.features.login.impl.util package io.element.android.features.login.impl.util
import io.element.android.features.login.impl.changeaccountprovider.item.AccountProviderItem
object LoginConstants { object LoginConstants {
const val MATRIX_ORG_URL = "matrix.org"
const val DEFAULT_HOMESERVER_URL = "matrix.org" // TODO Oidc "synapse-oidc.lab.element.dev" const val DEFAULT_HOMESERVER_URL = "matrix.org" // TODO Oidc "synapse-oidc.lab.element.dev"
const val SLIDING_SYNC_READ_MORE_URL = "https://github.com/matrix-org/sliding-sync/blob/main/docs/Landing.md" const val SLIDING_SYNC_READ_MORE_URL = "https://github.com/matrix-org/sliding-sync/blob/main/docs/Landing.md"
} }
val defaultAccountProviderItem = AccountProviderItem(
title = LoginConstants.DEFAULT_HOMESERVER_URL,
subtitle = null,
isPublic = LoginConstants.DEFAULT_HOMESERVER_URL == LoginConstants.MATRIX_ORG_URL,
isMatrixOrg = LoginConstants.DEFAULT_HOMESERVER_URL == LoginConstants.MATRIX_ORG_URL,
)

Loading…
Cancel
Save