ganfra
2 years ago
25 changed files with 385 additions and 304 deletions
@ -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) } |
||||
} |
||||
} |
@ -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("", "") |
||||
} |
||||
} |
@ -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 |
||||
} |
@ -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, |
||||
) |
||||
} |
||||
} |
@ -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) |
||||
} |
||||
} |
@ -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 |
||||
} |
@ -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) |
||||
} |
||||
} |
||||
} |
||||
} |
@ -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 |
||||
} |
@ -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 |
||||
} |
@ -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 |
||||
) |
||||
} |
||||
} |
@ -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() |
||||
} |
||||
} |
@ -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("", "") |
||||
} |
||||
} |
@ -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, |
||||
) |
||||
} |
||||
} |
||||
} |
@ -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 |
@ -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) |
||||
} |
||||
) |
||||
} |
Loading…
Reference in new issue