Browse Source

Migrate Login to new architecture and make some adjustments

feature/bma/flipper
ganfra 2 years ago
parent
commit
6a5bcf7058
  1. 3
      app/src/main/java/io/element/android/x/di/AppComponent.kt
  2. 6
      app/src/main/java/io/element/android/x/node/NotLoggedInFlowNode.kt
  3. 28
      features/login/src/main/java/io/element/android/x/features/login/LoginFlowNode.kt
  4. 67
      features/login/src/main/java/io/element/android/x/features/login/LoginViewModel.kt
  5. 26
      features/login/src/main/java/io/element/android/x/features/login/LoginViewState.kt
  6. 6
      features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerEvents.kt
  7. 47
      features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerNode.kt
  8. 47
      features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerPresenter.kt
  9. 10
      features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerState.kt
  10. 45
      features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerView.kt
  11. 51
      features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerViewModel.kt
  12. 13
      features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerViewState.kt
  13. 2
      features/login/src/main/java/io/element/android/x/features/login/error/ErrorFormatter.kt
  14. 8
      features/login/src/main/java/io/element/android/x/features/login/root/LoginRootEvents.kt
  15. 64
      features/login/src/main/java/io/element/android/x/features/login/root/LoginRootNode.kt
  16. 69
      features/login/src/main/java/io/element/android/x/features/login/root/LoginRootPresenter.kt
  17. 76
      features/login/src/main/java/io/element/android/x/features/login/root/LoginRootScreen.kt
  18. 32
      features/login/src/main/java/io/element/android/x/features/login/root/LoginRootState.kt
  19. 20
      features/onboarding/src/main/java/io/element/android/x/features/onboarding/OnBoardingScreen.kt
  20. 15
      features/onboarding/src/main/java/io/element/android/x/features/onboarding/OnBoardingViewModel.kt
  21. 7
      features/onboarding/src/main/java/io/element/android/x/features/onboarding/OnBoardingViewState.kt
  22. 1
      features/onboarding/src/main/java/io/element/android/x/features/onboarding/SplashCarouselStateFactory.kt
  23. 2
      features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListNode.kt
  24. 31
      libraries/architecture/src/main/java/io/element/android/x/architecture/Async.kt
  25. 13
      libraries/architecture/src/main/java/io/element/android/x/architecture/PresenterConnector.kt

3
app/src/main/java/io/element/android/x/di/AppComponent.kt

@ -4,11 +4,12 @@ import android.content.Context @@ -4,11 +4,12 @@ import android.content.Context
import com.squareup.anvil.annotations.MergeComponent
import dagger.BindsInstance
import dagger.Component
import io.element.android.x.architecture.NodeFactoriesBindings
import io.element.android.x.architecture.viewmodel.DaggerMavericksBindings
@SingleIn(AppScope::class)
@MergeComponent(AppScope::class)
interface AppComponent : DaggerMavericksBindings {
interface AppComponent : DaggerMavericksBindings, NodeFactoriesBindings {
@Component.Factory
interface Factory {

6
app/src/main/java/io/element/android/x/node/NotLoggedInFlowNode.kt

@ -8,10 +8,10 @@ import com.bumble.appyx.core.lifecycle.subscribe @@ -8,10 +8,10 @@ import com.bumble.appyx.core.lifecycle.subscribe
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.node.ParentNode
import com.bumble.appyx.core.node.node
import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.replace
import io.element.android.x.architecture.viewmodel.viewModelSupportNode
import io.element.android.x.features.login.node.LoginFlowNode
import io.element.android.x.features.login.LoginFlowNode
import io.element.android.x.features.onboarding.OnBoardingScreen
import kotlinx.parcelize.Parcelize
import timber.log.Timber
@ -44,7 +44,7 @@ class NotLoggedInFlowNode( @@ -44,7 +44,7 @@ class NotLoggedInFlowNode(
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
return when (navTarget) {
NavTarget.OnBoarding -> viewModelSupportNode(buildContext) {
NavTarget.OnBoarding -> node(buildContext) {
OnBoardingScreen(
onSignIn = { backstack.replace(NavTarget.LoginFlow) }
)

28
features/login/src/main/java/io/element/android/x/features/login/node/LoginFlowNode.kt → features/login/src/main/java/io/element/android/x/features/login/LoginFlowNode.kt

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
package io.element.android.x.features.login.node
package io.element.android.x.features.login
import android.os.Parcelable
import androidx.compose.runtime.Composable
@ -8,11 +8,10 @@ import com.bumble.appyx.core.modality.BuildContext @@ -8,11 +8,10 @@ import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.node.ParentNode
import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.pop
import com.bumble.appyx.navmodel.backstack.operation.push
import io.element.android.x.architecture.viewmodel.viewModelSupportNode
import io.element.android.x.features.login.LoginScreen
import io.element.android.x.features.login.changeserver.ChangeServerScreen
import io.element.android.x.architecture.createNode
import io.element.android.x.features.login.changeserver.ChangeServerNode
import io.element.android.x.features.login.root.LoginRootNode
import kotlinx.parcelize.Parcelize
class LoginFlowNode(
@ -26,6 +25,12 @@ class LoginFlowNode( @@ -26,6 +25,12 @@ class LoginFlowNode(
buildContext = buildContext
) {
private val loginRootCallback = object : LoginRootNode.Callback {
override fun onChangeHomeServer() {
backstack.push(NavTarget.ChangeServer)
}
}
sealed interface NavTarget : Parcelable {
@Parcelize
object Root : NavTarget
@ -36,16 +41,8 @@ class LoginFlowNode( @@ -36,16 +41,8 @@ class LoginFlowNode(
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
return when (navTarget) {
NavTarget.Root -> viewModelSupportNode(buildContext) {
LoginScreen(
onChangeServer = { backstack.push(NavTarget.ChangeServer) }
)
}
NavTarget.ChangeServer -> viewModelSupportNode(buildContext) {
ChangeServerScreen(
onChangeServerSuccess = { backstack.pop() }
)
}
NavTarget.Root -> createNode<LoginRootNode>(buildContext, plugins = listOf(loginRootCallback))
NavTarget.ChangeServer -> createNode<ChangeServerNode>(buildContext)
}
}
@ -53,5 +50,4 @@ class LoginFlowNode( @@ -53,5 +50,4 @@ class LoginFlowNode(
override fun View(modifier: Modifier) {
Children(navModel = backstack)
}
}

67
features/login/src/main/java/io/element/android/x/features/login/LoginViewModel.kt

@ -1,67 +0,0 @@ @@ -1,67 +0,0 @@
package io.element.android.x.features.login
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.snapshotFlow
import com.airbnb.mvrx.MavericksViewModel
import com.airbnb.mvrx.MavericksViewModelFactory
import com.airbnb.mvrx.Uninitialized
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.x.anvilannotations.ContributesViewModel
import io.element.android.x.architecture.viewmodel.daggerMavericksViewModelFactory
import io.element.android.x.di.AppScope
import io.element.android.x.matrix.Matrix
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
@ContributesViewModel(AppScope::class)
class LoginViewModel @AssistedInject constructor(
private val matrix: Matrix,
@Assisted initialState: LoginViewState) :
MavericksViewModel<LoginViewState>(initialState) {
companion object : MavericksViewModelFactory<LoginViewModel, LoginViewState> by daggerMavericksViewModelFactory()
var formState = mutableStateOf(LoginFormState.Default)
private set
init {
snapshotFlow { formState.value }
.onEach {
setState { copy(formState = it) }
}.launchIn(viewModelScope)
}
fun onResume() {
val currentHomeserver = matrix.getHomeserverOrDefault()
setState {
copy(
homeserver = currentHomeserver
)
}
}
fun onSubmit() {
viewModelScope.launch {
suspend {
val state = awaitState()
// Ensure the server is provided to the Rust SDK
matrix.setHomeserver(state.homeserver)
matrix.login(state.formState.login.trim(), state.formState.password.trim())
}.execute {
copy(loggedInSessionId = it)
}
}
}
fun onSetPassword(password: String) {
formState.value = formState.value.copy(password = password)
setState { copy(loggedInSessionId = Uninitialized) }
}
fun onSetName(name: String) {
formState.value = formState.value.copy(login = name)
setState { copy(loggedInSessionId = Uninitialized) }
}
}

26
features/login/src/main/java/io/element/android/x/features/login/LoginViewState.kt

@ -1,26 +0,0 @@ @@ -1,26 +0,0 @@
package io.element.android.x.features.login
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.MavericksState
import com.airbnb.mvrx.Uninitialized
import io.element.android.x.matrix.core.SessionId
data class LoginViewState(
val homeserver: String = "",
val loggedInSessionId: Async<SessionId> = Uninitialized,
val formState: LoginFormState = LoginFormState.Default,
) : MavericksState {
val submitEnabled =
formState.login.isNotEmpty() && formState.password.isNotEmpty() && loggedInSessionId !is Loading
}
data class LoginFormState(
val login: String,
val password: String
) {
companion object {
val Default = LoginFormState("", "")
}
}

6
features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerEvents.kt

@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
package io.element.android.x.features.login.changeserver
sealed interface ChangeServerEvents {
data class SetServer(val server: String) : ChangeServerEvents
object Submit: ChangeServerEvents
}

47
features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerNode.kt

@ -0,0 +1,47 @@ @@ -0,0 +1,47 @@
package io.element.android.x.features.login.changeserver
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.x.anvilannotations.ContributesNode
import io.element.android.x.architecture.presenterConnector
import io.element.android.x.di.AppScope
@ContributesNode(AppScope::class)
class ChangeServerNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
private val presenter: ChangeServerPresenter,
) : Node(buildContext, plugins = plugins) {
private val presenterConnector = presenterConnector(presenter)
private fun onChangeServer(server: String) {
presenterConnector.emitEvent(ChangeServerEvents.SetServer(server))
}
private fun onSubmit() {
presenterConnector.emitEvent(ChangeServerEvents.Submit)
}
private fun onSuccess() {
navigateUp()
}
@Composable
override fun View(modifier: Modifier) {
val state by presenterConnector.stateFlow.collectAsState()
ChangeServerView(
state = state,
onChangeServer = this::onChangeServer,
onChangeServerSubmit = this::onSubmit,
onChangeServerSuccess = this::onSuccess,
)
}
}

47
features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerPresenter.kt

@ -0,0 +1,47 @@ @@ -0,0 +1,47 @@
package io.element.android.x.features.login.changeserver
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import io.element.android.x.architecture.Async
import io.element.android.x.architecture.Presenter
import io.element.android.x.architecture.execute
import io.element.android.x.matrix.Matrix
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch
import javax.inject.Inject
class ChangeServerPresenter @Inject constructor(private val matrix: Matrix) : Presenter<ChangeServerState, ChangeServerEvents> {
@Composable
override fun present(events: Flow<ChangeServerEvents>): ChangeServerState {
val homeserver = rememberSaveable {
mutableStateOf(matrix.getHomeserverOrDefault())
}
val changeServerAction: MutableState<Async<Unit>> = remember {
mutableStateOf(Async.Uninitialized)
}
LaunchedEffect(Unit) {
events.collect { event ->
when (event) {
is ChangeServerEvents.SetServer -> homeserver.value = event.server
ChangeServerEvents.Submit -> submit(homeserver.value, changeServerAction)
}
}
}
return ChangeServerState(
homeserver = homeserver.value,
changeServerAction = changeServerAction.value
)
}
private fun CoroutineScope.submit(homeserver: String, changeServerAction: MutableState<Async<Unit>>) = launch {
suspend {
matrix.setHomeserver(homeserver)
}.execute(changeServerAction)
}
}

10
features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerState.kt

@ -0,0 +1,10 @@ @@ -0,0 +1,10 @@
package io.element.android.x.features.login.changeserver
import io.element.android.x.architecture.Async
data class ChangeServerState(
val homeserver: String = "",
val changeServerAction: Async<Unit> = Async.Uninitialized,
) {
val submitEnabled = homeserver.isNotEmpty() && changeServerAction !is Async.Loading
}

45
features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerScreen.kt → features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerView.kt

@ -26,6 +26,7 @@ import androidx.compose.material3.Surface @@ -26,6 +26,7 @@ import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
@ -35,33 +36,17 @@ import androidx.compose.ui.text.style.TextAlign @@ -35,33 +36,17 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.compose.collectAsState
import com.airbnb.mvrx.compose.mavericksViewModel
import io.element.android.x.architecture.Async
import io.element.android.x.core.compose.textFieldState
import io.element.android.x.designsystem.ElementXTheme
import io.element.android.x.designsystem.components.VectorIcon
import io.element.android.x.features.login.R
import io.element.android.x.features.login.error.changeServerError
@Composable
fun ChangeServerScreen(
viewModel: ChangeServerViewModel = mavericksViewModel(),
onChangeServerSuccess: () -> Unit = { }
) {
val state: ChangeServerViewState by viewModel.collectAsState()
ChangeServerContent(
state = state,
onChangeServer = viewModel::setServer,
onChangeServerSubmit = viewModel::setServerSubmit,
onChangeServerSuccess = onChangeServerSuccess
)
}
@Composable
fun ChangeServerContent(
state: ChangeServerViewState,
fun ChangeServerView(
state: ChangeServerState,
modifier: Modifier = Modifier,
onChangeServer: (String) -> Unit = {},
onChangeServerSubmit: () -> Unit = {},
@ -85,7 +70,7 @@ fun ChangeServerContent( @@ -85,7 +70,7 @@ fun ChangeServerContent(
)
.padding(horizontal = 16.dp)
) {
val isError = state.changeServerAction is Fail
val isError = state.changeServerAction is Async.Failure
Box(
modifier = Modifier
.padding(top = 99.dp)
@ -126,12 +111,16 @@ fun ChangeServerContent( @@ -126,12 +111,16 @@ fun ChangeServerContent(
fontSize = 16.sp,
color = MaterialTheme.colorScheme.secondary
)
var homeserverFieldState by textFieldState(stateValue = state.homeserver)
OutlinedTextField(
value = state.homeserver,
value = homeserverFieldState,
modifier = Modifier
.fillMaxWidth()
.padding(top = 200.dp),
onValueChange = onChangeServer,
onValueChange = {
homeserverFieldState = it
onChangeServer(it)
},
label = {
Text(text = "Server")
},
@ -144,7 +133,7 @@ fun ChangeServerContent( @@ -144,7 +133,7 @@ fun ChangeServerContent(
onDone = { onChangeServerSubmit() }
)
)
if (state.changeServerAction is Fail) {
if (state.changeServerAction is Async.Failure) {
Text(
text = changeServerError(
state.homeserver,
@ -164,11 +153,11 @@ fun ChangeServerContent( @@ -164,11 +153,11 @@ fun ChangeServerContent(
) {
Text(text = "Continue")
}
if (state.changeServerAction is Success) {
if (state.changeServerAction is Async.Success) {
onChangeServerSuccess()
}
}
if (state.changeServerAction is Loading) {
if (state.changeServerAction is Async.Loading) {
CircularProgressIndicator(
modifier = Modifier.align(Alignment.Center)
)
@ -181,8 +170,8 @@ fun ChangeServerContent( @@ -181,8 +170,8 @@ fun ChangeServerContent(
@Preview
fun ChangeServerContentPreview() {
ElementXTheme {
ChangeServerContent(
state = ChangeServerViewState(homeserver = "matrix.org"),
ChangeServerView(
state = ChangeServerState(homeserver = "matrix.org"),
)
}
}

51
features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerViewModel.kt

@ -1,51 +0,0 @@ @@ -1,51 +0,0 @@
package io.element.android.x.features.login.changeserver
import com.airbnb.mvrx.MavericksViewModel
import com.airbnb.mvrx.MavericksViewModelFactory
import com.airbnb.mvrx.Uninitialized
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.x.anvilannotations.ContributesViewModel
import io.element.android.x.architecture.viewmodel.daggerMavericksViewModelFactory
import io.element.android.x.di.AppScope
import io.element.android.x.matrix.Matrix
import kotlinx.coroutines.launch
@ContributesViewModel(AppScope::class)
class ChangeServerViewModel @AssistedInject constructor(
private val matrix: Matrix,
@Assisted initialState: ChangeServerViewState
) :
MavericksViewModel<ChangeServerViewState>(initialState) {
companion object :
MavericksViewModelFactory<ChangeServerViewModel, ChangeServerViewState> by daggerMavericksViewModelFactory()
init {
setState {
copy(
homeserver = matrix.getHomeserverOrDefault()
)
}
}
fun setServer(server: String) {
setState {
copy(
homeserver = server,
changeServerAction = Uninitialized,
)
}
}
fun setServerSubmit() {
viewModelScope.launch {
suspend {
val state = awaitState()
matrix.setHomeserver(state.homeserver)
}.execute {
copy(changeServerAction = it)
}
}
}
}

13
features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerViewState.kt

@ -1,13 +0,0 @@ @@ -1,13 +0,0 @@
package io.element.android.x.features.login.changeserver
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.MavericksState
import com.airbnb.mvrx.Uninitialized
data class ChangeServerViewState(
val homeserver: String = "",
val changeServerAction: Async<Unit> = Uninitialized,
) : MavericksState {
val submitEnabled = homeserver.isNotEmpty() && changeServerAction !is Loading
}

2
features/login/src/main/java/io/element/android/x/features/login/error/ErrorFormatter.kt

@ -3,8 +3,8 @@ package io.element.android.x.features.login.error @@ -3,8 +3,8 @@ package io.element.android.x.features.login.error
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import io.element.android.x.core.uri.isValidUrl
import io.element.android.x.features.login.root.LoginFormState
import io.element.android.x.element.resources.R as ElementR
import io.element.android.x.features.login.LoginFormState
@Composable
fun loginError(

8
features/login/src/main/java/io/element/android/x/features/login/root/LoginRootEvents.kt

@ -0,0 +1,8 @@ @@ -0,0 +1,8 @@
package io.element.android.x.features.login.root
sealed interface LoginRootEvents {
object RefreshHomeServer : LoginRootEvents
data class SetLogin(val login: String) : LoginRootEvents
data class SetPassword(val password: String) : LoginRootEvents
object Submit : LoginRootEvents
}

64
features/login/src/main/java/io/element/android/x/features/login/root/LoginRootNode.kt

@ -0,0 +1,64 @@ @@ -0,0 +1,64 @@
package io.element.android.x.features.login.root
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import com.bumble.appyx.core.lifecycle.subscribe
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.x.anvilannotations.ContributesNode
import io.element.android.x.architecture.presenterConnector
import io.element.android.x.di.AppScope
@ContributesNode(AppScope::class)
class LoginRootNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
private val presenter: LoginRootPresenter,
) : Node(buildContext, plugins = plugins) {
private val presenterConnector = presenterConnector(presenter)
init {
lifecycle.subscribe(
onResume = { presenterConnector.emitEvent(LoginRootEvents.RefreshHomeServer) }
)
}
interface Callback : Plugin {
fun onChangeHomeServer()
}
private fun onChangeHomeServer() {
plugins<Callback>().forEach { it.onChangeHomeServer() }
}
private fun onLoginChanged(login: String) {
presenterConnector.emitEvent(LoginRootEvents.SetLogin(login))
}
private fun onPasswordChanged(password: String) {
presenterConnector.emitEvent(LoginRootEvents.SetPassword(password))
}
private fun onSubmit() {
presenterConnector.emitEvent(LoginRootEvents.Submit)
}
@Composable
override fun View(modifier: Modifier) {
val state by presenterConnector.stateFlow.collectAsState()
LoginRootScreen(
state = state,
onChangeServer = this::onChangeHomeServer,
onLoginChanged = this::onLoginChanged,
onPasswordChanged = this::onPasswordChanged,
onSubmitClicked = this::onSubmit
)
}
}

69
features/login/src/main/java/io/element/android/x/features/login/root/LoginRootPresenter.kt

@ -0,0 +1,69 @@ @@ -0,0 +1,69 @@
package io.element.android.x.features.login.root
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import io.element.android.x.architecture.Presenter
import io.element.android.x.matrix.Matrix
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch
import javax.inject.Inject
class LoginRootPresenter @Inject constructor(private val matrix: Matrix) : Presenter<LoginRootState, LoginRootEvents> {
@Composable
override fun present(events: Flow<LoginRootEvents>): LoginRootState {
val homeserver = rememberSaveable {
mutableStateOf(matrix.getHomeserverOrDefault())
}
val loggedInState: MutableState<LoggedInState> = remember {
mutableStateOf(LoggedInState.NotLoggedIn)
}
val formState = rememberSaveable {
mutableStateOf(LoginFormState.Default)
}
LaunchedEffect(Unit) {
events.collect { event ->
when (event) {
LoginRootEvents.RefreshHomeServer -> refreshHomeServer(homeserver)
is LoginRootEvents.SetLogin -> updateFormState(formState) {
copy(login = event.login)
}
is LoginRootEvents.SetPassword -> updateFormState(formState) {
copy(password = event.password)
}
LoginRootEvents.Submit -> submit(homeserver.value, formState.value, loggedInState)
}
}
}
return LoginRootState(
homeserver = homeserver.value,
loggedInState = loggedInState.value,
formState = formState.value
)
}
private fun CoroutineScope.submit(homeserver: String, formState: LoginFormState, loggedInState: MutableState<LoggedInState>) = launch {
loggedInState.value = LoggedInState.LoggingIn
try {
matrix.setHomeserver(homeserver)
val sessionId = matrix.login(formState.login.trim(), formState.password.trim())
loggedInState.value = LoggedInState.LoggedIn(sessionId)
} catch (failure: Throwable) {
loggedInState.value = LoggedInState.ErrorLoggingIn(failure)
}
}
private fun updateFormState(formState: MutableState<LoginFormState>, updateLambda: LoginFormState.() -> LoginFormState) {
formState.value = updateLambda(formState.value)
}
private fun refreshHomeServer(homeserver: MutableState<String>) {
homeserver.value = matrix.getHomeserverOrDefault()
}
}

76
features/login/src/main/java/io/element/android/x/features/login/LoginScreen.kt → features/login/src/main/java/io/element/android/x/features/login/root/LoginRootScreen.kt

@ -1,6 +1,4 @@ @@ -1,6 +1,4 @@
@file:OptIn(ExperimentalMaterial3Api::class)
package io.element.android.x.features.login
package io.element.android.x.features.login.root
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@ -26,7 +24,6 @@ import androidx.compose.material3.OutlinedTextField @@ -26,7 +24,6 @@ import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@ -42,44 +39,15 @@ import androidx.compose.ui.text.style.TextAlign @@ -42,44 +39,15 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.compose.collectAsState
import com.airbnb.mvrx.compose.mavericksViewModel
import io.element.android.x.core.compose.textFieldState
import io.element.android.x.designsystem.ElementXTheme
import io.element.android.x.features.login.error.loginError
import io.element.android.x.matrix.core.SessionId
import timber.log.Timber
@Composable
fun LoginScreen(
viewModel: LoginViewModel = mavericksViewModel(),
onChangeServer: () -> Unit = { },
onLoginWithSuccess: (SessionId) -> Unit = { },
) {
val state: LoginViewState by viewModel.collectAsState()
val formState: LoginFormState by viewModel.formState
LaunchedEffect(key1 = Unit) {
Timber.d("resume")
viewModel.onResume()
}
LoginContent(
state = state,
formState = formState,
onChangeServer = onChangeServer,
onLoginChanged = viewModel::onSetName,
onPasswordChanged = viewModel::onSetPassword,
onSubmitClicked = viewModel::onSubmit,
onLoginWithSuccess = onLoginWithSuccess
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun LoginContent(
state: LoginViewState,
formState: LoginFormState,
fun LoginRootScreen(
state: LoginRootState,
modifier: Modifier = Modifier,
onChangeServer: () -> Unit = {},
onLoginChanged: (String) -> Unit = {},
@ -98,6 +66,9 @@ fun LoginContent( @@ -98,6 +66,9 @@ fun LoginContent(
.imePadding()
) {
val scrollState = rememberScrollState()
var loginFieldState by textFieldState(stateValue = state.formState.login)
var passwordFieldState by textFieldState(stateValue = state.formState.password)
Column(
modifier = Modifier
.verticalScroll(
@ -105,7 +76,7 @@ fun LoginContent( @@ -105,7 +76,7 @@ fun LoginContent(
)
.padding(horizontal = 16.dp),
) {
val isError = state.loggedInSessionId is Fail
val isError = state.loggedInState is LoggedInState.ErrorLoggingIn
// Title
Text(
text = "Welcome back",
@ -146,30 +117,36 @@ fun LoginContent( @@ -146,30 +117,36 @@ fun LoginContent(
)
}
OutlinedTextField(
value = formState.login,
value = loginFieldState,
modifier = Modifier
.fillMaxWidth()
.padding(top = 60.dp),
label = {
Text(text = "Email or username")
},
onValueChange = onLoginChanged,
onValueChange = {
loginFieldState = it
onLoginChanged(it)
},
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Email,
imeAction = ImeAction.Next
),
)
var passwordVisible by remember { mutableStateOf(false) }
if (state.loggedInSessionId is Loading) {
if (state.loggedInState is LoggedInState.LoggingIn) {
// Ensure password is hidden when user submits the form
passwordVisible = false
}
OutlinedTextField(
value = formState.password,
value = passwordFieldState,
modifier = Modifier
.fillMaxWidth()
.padding(top = 24.dp),
onValueChange = onPasswordChanged,
onValueChange = {
passwordFieldState = it
onPasswordChanged(it)
},
label = {
Text(text = "Password")
},
@ -193,9 +170,9 @@ fun LoginContent( @@ -193,9 +170,9 @@ fun LoginContent(
onDone = { onSubmitClicked() }
),
)
if (state.loggedInSessionId is Fail) {
if (state.loggedInState is LoggedInState.ErrorLoggingIn) {
Text(
text = loginError(state.formState, state.loggedInSessionId.error),
text = loginError(state.formState, state.loggedInState.failure),
color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(start = 16.dp)
@ -212,12 +189,12 @@ fun LoginContent( @@ -212,12 +189,12 @@ fun LoginContent(
) {
Text(text = "Continue")
}
when (val loggedInSessionId = state.loggedInSessionId) {
is Success -> onLoginWithSuccess(loggedInSessionId())
when (val loggedInState = state.loggedInState) {
is LoggedInState.LoggedIn -> onLoginWithSuccess(loggedInState.sessionId)
else -> Unit
}
}
if (state.loggedInSessionId is Loading) {
if (state.loggedInState is LoggedInState.LoggingIn) {
CircularProgressIndicator(
modifier = Modifier.align(Alignment.Center)
)
@ -230,11 +207,10 @@ fun LoginContent( @@ -230,11 +207,10 @@ fun LoginContent(
@Preview
fun LoginContentPreview() {
ElementXTheme(darkTheme = false) {
LoginContent(
state = LoginViewState(
LoginRootScreen(
state = LoginRootState(
homeserver = "matrix.org",
),
formState = LoginFormState("", "")
)
}
}

32
features/login/src/main/java/io/element/android/x/features/login/root/LoginRootState.kt

@ -0,0 +1,32 @@ @@ -0,0 +1,32 @@
package io.element.android.x.features.login.root
import android.os.Parcelable
import io.element.android.x.matrix.core.SessionId
import kotlinx.parcelize.Parcelize
data class LoginRootState(
val homeserver: String = "",
val loggedInState: LoggedInState = LoggedInState.NotLoggedIn,
val formState: LoginFormState = LoginFormState.Default,
) {
val submitEnabled =
formState.login.isNotEmpty() && formState.password.isNotEmpty() && loggedInState != LoggedInState.LoggingIn
}
sealed interface LoggedInState {
object NotLoggedIn : LoggedInState
object LoggingIn : LoggedInState
data class ErrorLoggingIn(val failure: Throwable) : LoggedInState
data class LoggedIn(val sessionId: SessionId) : LoggedInState
}
@Parcelize
data class LoginFormState(
val login: String,
val password: String
) : Parcelable {
companion object {
val Default = LoginFormState("", "")
}
}

20
features/onboarding/src/main/java/io/element/android/x/features/onboarding/OnBoardingScreen.kt

@ -29,8 +29,6 @@ import androidx.compose.ui.text.font.FontWeight @@ -29,8 +29,6 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.airbnb.mvrx.compose.collectAsState
import com.airbnb.mvrx.compose.mavericksViewModel
import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.pager.HorizontalPager
import com.google.accompanist.pager.HorizontalPagerIndicator
@ -39,25 +37,9 @@ import io.element.android.x.designsystem.components.VectorButton @@ -39,25 +37,9 @@ import io.element.android.x.designsystem.components.VectorButton
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@Composable
fun OnBoardingScreen(
viewModel: OnBoardingViewModel = mavericksViewModel(),
onSignUp: () -> Unit = { },
onSignIn: () -> Unit = { },
) {
val state: OnBoardingViewState by viewModel.collectAsState()
OnBoardingContent(
state,
onPageChanged = viewModel::onPageChanged,
onSignUp = onSignUp,
onSignIn = onSignIn,
)
}
@OptIn(ExperimentalPagerApi::class)
@Composable
fun OnBoardingContent(
state: OnBoardingViewState,
fun OnBoardingScreen(
modifier: Modifier = Modifier,
onPageChanged: (Int) -> Unit = {},
onSignUp: () -> Unit = {},

15
features/onboarding/src/main/java/io/element/android/x/features/onboarding/OnBoardingViewModel.kt

@ -1,15 +0,0 @@ @@ -1,15 +0,0 @@
package io.element.android.x.features.onboarding
import com.airbnb.mvrx.MavericksViewModel
class OnBoardingViewModel(initialState: OnBoardingViewState) :
MavericksViewModel<OnBoardingViewState>(initialState) {
fun onPageChanged(page: Int) {
setState {
copy(
currentPage = page,
)
}
}
}

7
features/onboarding/src/main/java/io/element/android/x/features/onboarding/OnBoardingViewState.kt

@ -1,7 +0,0 @@ @@ -1,7 +0,0 @@
package io.element.android.x.features.onboarding
import com.airbnb.mvrx.MavericksState
data class OnBoardingViewState(
val currentPage: Int = 0,
) : MavericksState

1
features/onboarding/src/main/java/io/element/android/x/features/onboarding/SplashCarouselStateFactory.kt

@ -6,6 +6,7 @@ import io.element.android.x.element.resources.R as ElementR @@ -6,6 +6,7 @@ import io.element.android.x.element.resources.R as ElementR
class SplashCarouselStateFactory {
fun create(): SplashCarouselState {
val lightTheme = true
fun background(@DrawableRes lightDrawable: Int) =
if (lightTheme) lightDrawable else R.drawable.bg_color_background

2
features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListNode.kt

@ -27,7 +27,7 @@ class RoomListNode @AssistedInject constructor( @@ -27,7 +27,7 @@ class RoomListNode @AssistedInject constructor(
fun onRoomClicked(roomId: RoomId)
}
private val connector by presenterConnector(presenter)
private val connector = presenterConnector(presenter)
private fun updateFilter(filter: String) {
connector.emitEvent(RoomListEvents.UpdateFilter(filter))

31
libraries/architecture/src/main/java/io/element/android/x/architecture/Async.kt

@ -0,0 +1,31 @@ @@ -0,0 +1,31 @@
package io.element.android.x.architecture
import androidx.compose.runtime.MutableState
sealed interface Async<out T> {
object Uninitialized : Async<Nothing>
data class Loading<out T>(val prevState: T? = null) : Async<T>
data class Failure<out T>(val error: Throwable) : Async<T>
data class Success<out T>(val state: T) : Async<T>
}
suspend fun <T> (suspend () -> T).execute(state: MutableState<Async<T>>) {
try {
state.value = Async.Loading()
state.value = Async.Success(this())
} catch (error: Throwable) {
state.value = Async.Failure(error)
}
}
suspend fun <T> (suspend () -> Result<T>).executeResult(state: MutableState<Async<T>>) {
state.value = Async.Loading()
this().fold(
onSuccess = {
state.value = Async.Success(it)
},
onFailure = {
state.value = Async.Failure(it)
}
)
}

13
libraries/architecture/src/main/java/io/element/android/x/architecture/LifecyclePresenterConnector.kt → libraries/architecture/src/main/java/io/element/android/x/architecture/PresenterConnector.kt

@ -8,21 +8,22 @@ import app.cash.molecule.launchMolecule @@ -8,21 +8,22 @@ import app.cash.molecule.launchMolecule
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
inline fun <reified State, reified Event> LifecycleOwner.presenterConnector(presenter: Presenter<State, Event>): Lazy<LifecyclePresenterConnector<State, Event>> = lazy {
inline fun <reified State, reified Event> LifecycleOwner.presenterConnector(presenter: Presenter<State, Event>): LifecyclePresenterConnector<State, Event> =
LifecyclePresenterConnector(lifecycleOwner = this, presenter = presenter)
}
class LifecyclePresenterConnector<State, Event>(lifecycleOwner: LifecycleOwner, presenter: Presenter<State, Event>) {
private val moleculeScope = CoroutineScope(lifecycleOwner.lifecycleScope.coroutineContext + AndroidUiDispatcher.Main)
private val eventFlow: MutableSharedFlow<Event> = MutableSharedFlow(extraBufferCapacity = 64)
private val mutableEventFlow: MutableSharedFlow<Event> = MutableSharedFlow(extraBufferCapacity = 64)
val stateFlow: StateFlow<State> = moleculeScope.launchMolecule(RecompositionClock.ContextClock) {
presenter.present(events = eventFlow)
val stateFlow: StateFlow<State> = moleculeScope.launchMolecule(RecompositionClock.Immediate) {
presenter.present(events = mutableEventFlow)
}
fun emitEvent(event: Event) {
eventFlow.tryEmit(event)
mutableEventFlow.tryEmit(event)
}
}
Loading…
Cancel
Save