Migrate Login to new architecture and make some adjustments
This commit is contained in:
parent
f4ac368d07
commit
9ed208950e
25 changed files with 385 additions and 304 deletions
|
|
@ -4,11 +4,12 @@ import android.content.Context
|
||||||
import com.squareup.anvil.annotations.MergeComponent
|
import com.squareup.anvil.annotations.MergeComponent
|
||||||
import dagger.BindsInstance
|
import dagger.BindsInstance
|
||||||
import dagger.Component
|
import dagger.Component
|
||||||
|
import io.element.android.x.architecture.NodeFactoriesBindings
|
||||||
import io.element.android.x.architecture.viewmodel.DaggerMavericksBindings
|
import io.element.android.x.architecture.viewmodel.DaggerMavericksBindings
|
||||||
|
|
||||||
@SingleIn(AppScope::class)
|
@SingleIn(AppScope::class)
|
||||||
@MergeComponent(AppScope::class)
|
@MergeComponent(AppScope::class)
|
||||||
interface AppComponent : DaggerMavericksBindings {
|
interface AppComponent : DaggerMavericksBindings, NodeFactoriesBindings {
|
||||||
|
|
||||||
@Component.Factory
|
@Component.Factory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,10 @@ import com.bumble.appyx.core.lifecycle.subscribe
|
||||||
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.node.ParentNode
|
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.BackStack
|
||||||
import com.bumble.appyx.navmodel.backstack.operation.replace
|
import com.bumble.appyx.navmodel.backstack.operation.replace
|
||||||
import io.element.android.x.architecture.viewmodel.viewModelSupportNode
|
import io.element.android.x.features.login.LoginFlowNode
|
||||||
import io.element.android.x.features.login.node.LoginFlowNode
|
|
||||||
import io.element.android.x.features.onboarding.OnBoardingScreen
|
import io.element.android.x.features.onboarding.OnBoardingScreen
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
@ -44,7 +44,7 @@ class NotLoggedInFlowNode(
|
||||||
|
|
||||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||||
return when (navTarget) {
|
return when (navTarget) {
|
||||||
NavTarget.OnBoarding -> viewModelSupportNode(buildContext) {
|
NavTarget.OnBoarding -> node(buildContext) {
|
||||||
OnBoardingScreen(
|
OnBoardingScreen(
|
||||||
onSignIn = { backstack.replace(NavTarget.LoginFlow) }
|
onSignIn = { backstack.replace(NavTarget.LoginFlow) }
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package io.element.android.x.features.login.node
|
package io.element.android.x.features.login
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
|
@ -8,11 +8,10 @@ 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.node.ParentNode
|
import com.bumble.appyx.core.node.ParentNode
|
||||||
import com.bumble.appyx.navmodel.backstack.BackStack
|
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||||
import com.bumble.appyx.navmodel.backstack.operation.pop
|
|
||||||
import com.bumble.appyx.navmodel.backstack.operation.push
|
import com.bumble.appyx.navmodel.backstack.operation.push
|
||||||
import io.element.android.x.architecture.viewmodel.viewModelSupportNode
|
import io.element.android.x.architecture.createNode
|
||||||
import io.element.android.x.features.login.LoginScreen
|
import io.element.android.x.features.login.changeserver.ChangeServerNode
|
||||||
import io.element.android.x.features.login.changeserver.ChangeServerScreen
|
import io.element.android.x.features.login.root.LoginRootNode
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
class LoginFlowNode(
|
class LoginFlowNode(
|
||||||
|
|
@ -26,6 +25,12 @@ class LoginFlowNode(
|
||||||
buildContext = buildContext
|
buildContext = buildContext
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
private val loginRootCallback = object : LoginRootNode.Callback {
|
||||||
|
override fun onChangeHomeServer() {
|
||||||
|
backstack.push(NavTarget.ChangeServer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
sealed interface NavTarget : Parcelable {
|
sealed interface NavTarget : Parcelable {
|
||||||
@Parcelize
|
@Parcelize
|
||||||
object Root : NavTarget
|
object Root : NavTarget
|
||||||
|
|
@ -36,16 +41,8 @@ class LoginFlowNode(
|
||||||
|
|
||||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||||
return when (navTarget) {
|
return when (navTarget) {
|
||||||
NavTarget.Root -> viewModelSupportNode(buildContext) {
|
NavTarget.Root -> createNode<LoginRootNode>(buildContext, plugins = listOf(loginRootCallback))
|
||||||
LoginScreen(
|
NavTarget.ChangeServer -> createNode<ChangeServerNode>(buildContext)
|
||||||
onChangeServer = { backstack.push(NavTarget.ChangeServer) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
NavTarget.ChangeServer -> viewModelSupportNode(buildContext) {
|
|
||||||
ChangeServerScreen(
|
|
||||||
onChangeServerSuccess = { backstack.pop() }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -53,5 +50,4 @@ class LoginFlowNode(
|
||||||
override fun View(modifier: Modifier) {
|
override fun View(modifier: Modifier) {
|
||||||
Children(navModel = backstack)
|
Children(navModel = backstack)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -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) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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("", "")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -26,6 +26,7 @@ import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
|
@ -35,33 +36,17 @@ import androidx.compose.ui.text.style.TextAlign
|
||||||
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 androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import com.airbnb.mvrx.Fail
|
import io.element.android.x.architecture.Async
|
||||||
import com.airbnb.mvrx.Loading
|
import io.element.android.x.core.compose.textFieldState
|
||||||
import com.airbnb.mvrx.Success
|
|
||||||
import com.airbnb.mvrx.compose.collectAsState
|
|
||||||
import com.airbnb.mvrx.compose.mavericksViewModel
|
|
||||||
import io.element.android.x.designsystem.ElementXTheme
|
import io.element.android.x.designsystem.ElementXTheme
|
||||||
import io.element.android.x.designsystem.components.VectorIcon
|
import io.element.android.x.designsystem.components.VectorIcon
|
||||||
import io.element.android.x.features.login.R
|
import io.element.android.x.features.login.R
|
||||||
import io.element.android.x.features.login.error.changeServerError
|
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
|
@Composable
|
||||||
fun ChangeServerContent(
|
fun ChangeServerView(
|
||||||
state: ChangeServerViewState,
|
state: ChangeServerState,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onChangeServer: (String) -> Unit = {},
|
onChangeServer: (String) -> Unit = {},
|
||||||
onChangeServerSubmit: () -> Unit = {},
|
onChangeServerSubmit: () -> Unit = {},
|
||||||
|
|
@ -85,7 +70,7 @@ fun ChangeServerContent(
|
||||||
)
|
)
|
||||||
.padding(horizontal = 16.dp)
|
.padding(horizontal = 16.dp)
|
||||||
) {
|
) {
|
||||||
val isError = state.changeServerAction is Fail
|
val isError = state.changeServerAction is Async.Failure
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(top = 99.dp)
|
.padding(top = 99.dp)
|
||||||
|
|
@ -126,12 +111,16 @@ fun ChangeServerContent(
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
color = MaterialTheme.colorScheme.secondary
|
color = MaterialTheme.colorScheme.secondary
|
||||||
)
|
)
|
||||||
|
var homeserverFieldState by textFieldState(stateValue = state.homeserver)
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = state.homeserver,
|
value = homeserverFieldState,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(top = 200.dp),
|
.padding(top = 200.dp),
|
||||||
onValueChange = onChangeServer,
|
onValueChange = {
|
||||||
|
homeserverFieldState = it
|
||||||
|
onChangeServer(it)
|
||||||
|
},
|
||||||
label = {
|
label = {
|
||||||
Text(text = "Server")
|
Text(text = "Server")
|
||||||
},
|
},
|
||||||
|
|
@ -144,7 +133,7 @@ fun ChangeServerContent(
|
||||||
onDone = { onChangeServerSubmit() }
|
onDone = { onChangeServerSubmit() }
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if (state.changeServerAction is Fail) {
|
if (state.changeServerAction is Async.Failure) {
|
||||||
Text(
|
Text(
|
||||||
text = changeServerError(
|
text = changeServerError(
|
||||||
state.homeserver,
|
state.homeserver,
|
||||||
|
|
@ -164,11 +153,11 @@ fun ChangeServerContent(
|
||||||
) {
|
) {
|
||||||
Text(text = "Continue")
|
Text(text = "Continue")
|
||||||
}
|
}
|
||||||
if (state.changeServerAction is Success) {
|
if (state.changeServerAction is Async.Success) {
|
||||||
onChangeServerSuccess()
|
onChangeServerSuccess()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (state.changeServerAction is Loading) {
|
if (state.changeServerAction is Async.Loading) {
|
||||||
CircularProgressIndicator(
|
CircularProgressIndicator(
|
||||||
modifier = Modifier.align(Alignment.Center)
|
modifier = Modifier.align(Alignment.Center)
|
||||||
)
|
)
|
||||||
|
|
@ -181,8 +170,8 @@ fun ChangeServerContent(
|
||||||
@Preview
|
@Preview
|
||||||
fun ChangeServerContentPreview() {
|
fun ChangeServerContentPreview() {
|
||||||
ElementXTheme {
|
ElementXTheme {
|
||||||
ChangeServerContent(
|
ChangeServerView(
|
||||||
state = ChangeServerViewState(homeserver = "matrix.org"),
|
state = ChangeServerState(homeserver = "matrix.org"),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
@ -3,8 +3,8 @@ package io.element.android.x.features.login.error
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import io.element.android.x.core.uri.isValidUrl
|
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.element.resources.R as ElementR
|
||||||
import io.element.android.x.features.login.LoginFormState
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun loginError(
|
fun loginError(
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
@file:OptIn(ExperimentalMaterial3Api::class)
|
package io.element.android.x.features.login.root
|
||||||
|
|
||||||
package io.element.android.x.features.login
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
|
@ -26,7 +24,6 @@ import androidx.compose.material3.OutlinedTextField
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
|
@ -42,44 +39,15 @@ import androidx.compose.ui.text.style.TextAlign
|
||||||
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 androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import com.airbnb.mvrx.Fail
|
import io.element.android.x.core.compose.textFieldState
|
||||||
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.designsystem.ElementXTheme
|
import io.element.android.x.designsystem.ElementXTheme
|
||||||
import io.element.android.x.features.login.error.loginError
|
import io.element.android.x.features.login.error.loginError
|
||||||
import io.element.android.x.matrix.core.SessionId
|
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)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun LoginContent(
|
fun LoginRootScreen(
|
||||||
state: LoginViewState,
|
state: LoginRootState,
|
||||||
formState: LoginFormState,
|
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onChangeServer: () -> Unit = {},
|
onChangeServer: () -> Unit = {},
|
||||||
onLoginChanged: (String) -> Unit = {},
|
onLoginChanged: (String) -> Unit = {},
|
||||||
|
|
@ -98,6 +66,9 @@ fun LoginContent(
|
||||||
.imePadding()
|
.imePadding()
|
||||||
) {
|
) {
|
||||||
val scrollState = rememberScrollState()
|
val scrollState = rememberScrollState()
|
||||||
|
var loginFieldState by textFieldState(stateValue = state.formState.login)
|
||||||
|
var passwordFieldState by textFieldState(stateValue = state.formState.password)
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.verticalScroll(
|
.verticalScroll(
|
||||||
|
|
@ -105,7 +76,7 @@ fun LoginContent(
|
||||||
)
|
)
|
||||||
.padding(horizontal = 16.dp),
|
.padding(horizontal = 16.dp),
|
||||||
) {
|
) {
|
||||||
val isError = state.loggedInSessionId is Fail
|
val isError = state.loggedInState is LoggedInState.ErrorLoggingIn
|
||||||
// Title
|
// Title
|
||||||
Text(
|
Text(
|
||||||
text = "Welcome back",
|
text = "Welcome back",
|
||||||
|
|
@ -146,30 +117,36 @@ fun LoginContent(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = formState.login,
|
value = loginFieldState,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(top = 60.dp),
|
.padding(top = 60.dp),
|
||||||
label = {
|
label = {
|
||||||
Text(text = "Email or username")
|
Text(text = "Email or username")
|
||||||
},
|
},
|
||||||
onValueChange = onLoginChanged,
|
onValueChange = {
|
||||||
|
loginFieldState = it
|
||||||
|
onLoginChanged(it)
|
||||||
|
},
|
||||||
keyboardOptions = KeyboardOptions(
|
keyboardOptions = KeyboardOptions(
|
||||||
keyboardType = KeyboardType.Email,
|
keyboardType = KeyboardType.Email,
|
||||||
imeAction = ImeAction.Next
|
imeAction = ImeAction.Next
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
var passwordVisible by remember { mutableStateOf(false) }
|
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
|
// Ensure password is hidden when user submits the form
|
||||||
passwordVisible = false
|
passwordVisible = false
|
||||||
}
|
}
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = formState.password,
|
value = passwordFieldState,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(top = 24.dp),
|
.padding(top = 24.dp),
|
||||||
onValueChange = onPasswordChanged,
|
onValueChange = {
|
||||||
|
passwordFieldState = it
|
||||||
|
onPasswordChanged(it)
|
||||||
|
},
|
||||||
label = {
|
label = {
|
||||||
Text(text = "Password")
|
Text(text = "Password")
|
||||||
},
|
},
|
||||||
|
|
@ -193,9 +170,9 @@ fun LoginContent(
|
||||||
onDone = { onSubmitClicked() }
|
onDone = { onSubmitClicked() }
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
if (state.loggedInSessionId is Fail) {
|
if (state.loggedInState is LoggedInState.ErrorLoggingIn) {
|
||||||
Text(
|
Text(
|
||||||
text = loginError(state.formState, state.loggedInSessionId.error),
|
text = loginError(state.formState, state.loggedInState.failure),
|
||||||
color = MaterialTheme.colorScheme.error,
|
color = MaterialTheme.colorScheme.error,
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
modifier = Modifier.padding(start = 16.dp)
|
modifier = Modifier.padding(start = 16.dp)
|
||||||
|
|
@ -212,12 +189,12 @@ fun LoginContent(
|
||||||
) {
|
) {
|
||||||
Text(text = "Continue")
|
Text(text = "Continue")
|
||||||
}
|
}
|
||||||
when (val loggedInSessionId = state.loggedInSessionId) {
|
when (val loggedInState = state.loggedInState) {
|
||||||
is Success -> onLoginWithSuccess(loggedInSessionId())
|
is LoggedInState.LoggedIn -> onLoginWithSuccess(loggedInState.sessionId)
|
||||||
else -> Unit
|
else -> Unit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (state.loggedInSessionId is Loading) {
|
if (state.loggedInState is LoggedInState.LoggingIn) {
|
||||||
CircularProgressIndicator(
|
CircularProgressIndicator(
|
||||||
modifier = Modifier.align(Alignment.Center)
|
modifier = Modifier.align(Alignment.Center)
|
||||||
)
|
)
|
||||||
|
|
@ -230,11 +207,10 @@ fun LoginContent(
|
||||||
@Preview
|
@Preview
|
||||||
fun LoginContentPreview() {
|
fun LoginContentPreview() {
|
||||||
ElementXTheme(darkTheme = false) {
|
ElementXTheme(darkTheme = false) {
|
||||||
LoginContent(
|
LoginRootScreen(
|
||||||
state = LoginViewState(
|
state = LoginRootState(
|
||||||
homeserver = "matrix.org",
|
homeserver = "matrix.org",
|
||||||
),
|
),
|
||||||
formState = LoginFormState("", "")
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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("", "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -29,8 +29,6 @@ import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
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.ExperimentalPagerApi
|
||||||
import com.google.accompanist.pager.HorizontalPager
|
import com.google.accompanist.pager.HorizontalPager
|
||||||
import com.google.accompanist.pager.HorizontalPagerIndicator
|
import com.google.accompanist.pager.HorizontalPagerIndicator
|
||||||
|
|
@ -39,25 +37,9 @@ import io.element.android.x.designsystem.components.VectorButton
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
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)
|
@OptIn(ExperimentalPagerApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun OnBoardingContent(
|
fun OnBoardingScreen(
|
||||||
state: OnBoardingViewState,
|
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onPageChanged: (Int) -> Unit = {},
|
onPageChanged: (Int) -> Unit = {},
|
||||||
onSignUp: () -> Unit = {},
|
onSignUp: () -> Unit = {},
|
||||||
|
|
|
||||||
|
|
@ -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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
package io.element.android.x.features.onboarding
|
|
||||||
|
|
||||||
import com.airbnb.mvrx.MavericksState
|
|
||||||
|
|
||||||
data class OnBoardingViewState(
|
|
||||||
val currentPage: Int = 0,
|
|
||||||
) : MavericksState
|
|
||||||
|
|
@ -6,6 +6,7 @@ import io.element.android.x.element.resources.R as ElementR
|
||||||
class SplashCarouselStateFactory {
|
class SplashCarouselStateFactory {
|
||||||
fun create(): SplashCarouselState {
|
fun create(): SplashCarouselState {
|
||||||
val lightTheme = true
|
val lightTheme = true
|
||||||
|
|
||||||
fun background(@DrawableRes lightDrawable: Int) =
|
fun background(@DrawableRes lightDrawable: Int) =
|
||||||
if (lightTheme) lightDrawable else R.drawable.bg_color_background
|
if (lightTheme) lightDrawable else R.drawable.bg_color_background
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ class RoomListNode @AssistedInject constructor(
|
||||||
fun onRoomClicked(roomId: RoomId)
|
fun onRoomClicked(roomId: RoomId)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val connector by presenterConnector(presenter)
|
private val connector = presenterConnector(presenter)
|
||||||
|
|
||||||
private fun updateFilter(filter: String) {
|
private fun updateFilter(filter: String) {
|
||||||
connector.emitEvent(RoomListEvents.UpdateFilter(filter))
|
connector.emitEvent(RoomListEvents.UpdateFilter(filter))
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -8,21 +8,22 @@ import app.cash.molecule.launchMolecule
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
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)
|
LifecyclePresenterConnector(lifecycleOwner = this, presenter = presenter)
|
||||||
}
|
|
||||||
|
|
||||||
class LifecyclePresenterConnector<State, Event>(lifecycleOwner: LifecycleOwner, presenter: Presenter<State, Event>) {
|
class LifecyclePresenterConnector<State, Event>(lifecycleOwner: LifecycleOwner, presenter: Presenter<State, Event>) {
|
||||||
|
|
||||||
private val moleculeScope = CoroutineScope(lifecycleOwner.lifecycleScope.coroutineContext + AndroidUiDispatcher.Main)
|
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) {
|
val stateFlow: StateFlow<State> = moleculeScope.launchMolecule(RecompositionClock.Immediate) {
|
||||||
presenter.present(events = eventFlow)
|
presenter.present(events = mutableEventFlow)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun emitEvent(event: Event) {
|
fun emitEvent(event: Event) {
|
||||||
eventFlow.tryEmit(event)
|
mutableEventFlow.tryEmit(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue