Browse Source

Login: change server screen WIP

feature/bma/flipper
Benoit Marty 2 years ago
parent
commit
0840602069
  1. 21
      app/src/main/java/io/element/android/x/Navigation.kt
  2. 8
      features/login/src/main/java/io/element/android/x/features/login/LoginActions.kt
  3. 202
      features/login/src/main/java/io/element/android/x/features/login/LoginScreen.kt
  4. 41
      features/login/src/main/java/io/element/android/x/features/login/LoginViewModel.kt
  5. 7
      features/login/src/main/java/io/element/android/x/features/login/LoginViewState.kt
  6. 149
      features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerScreen.kt
  7. 32
      features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerViewModel.kt
  8. 13
      features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerViewState.kt
  9. 5
      features/onboarding/src/main/java/io/element/android/x/features/onboarding/OnBoardingActions.kt
  10. 1
      libraries/core/build.gradle
  11. 26
      libraries/core/src/main/java/io/element/android/x/core/compose/Keyboard.kt
  12. 10
      libraries/matrix/src/main/java/io/element/android/x/matrix/Matrix.kt

21
app/src/main/java/io/element/android/x/Navigation.kt

@ -5,11 +5,9 @@ import com.ramcosta.composedestinations.annotation.Destination @@ -5,11 +5,9 @@ import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootNavGraph
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import com.ramcosta.composedestinations.navigation.popUpTo
import io.element.android.x.destinations.LoginScreenNavigationDestination
import io.element.android.x.destinations.MessagesScreenNavigationDestination
import io.element.android.x.destinations.RoomListScreenNavigationDestination
import io.element.android.x.destinations.OnBoardingScreenNavigationDestination
import io.element.android.x.destinations.*
import io.element.android.x.features.login.LoginScreen
import io.element.android.x.features.login.changeserver.ChangeServerScreen
import io.element.android.x.features.messages.MessagesScreen
import io.element.android.x.features.onboarding.OnBoardingScreen
import io.element.android.x.features.roomlist.RoomListScreen
@ -32,6 +30,10 @@ fun OnBoardingScreenNavigation(navigator: DestinationsNavigator) { @@ -32,6 +30,10 @@ fun OnBoardingScreenNavigation(navigator: DestinationsNavigator) {
@Composable
fun LoginScreenNavigation(navigator: DestinationsNavigator) {
LoginScreen(
homeserver = "matrix.org",
onChangeServer = {
navigator.navigate(ChangeServerScreenNavigationDestination)
},
onLoginWithSuccess = {
navigator.navigate(RoomListScreenNavigationDestination) {
popUpTo(OnBoardingScreenNavigationDestination) {
@ -42,6 +44,17 @@ fun LoginScreenNavigation(navigator: DestinationsNavigator) { @@ -42,6 +44,17 @@ fun LoginScreenNavigation(navigator: DestinationsNavigator) {
)
}
// TODO Create a subgraph in Login module
@Destination
@Composable
fun ChangeServerScreenNavigation(navigator: DestinationsNavigator) {
ChangeServerScreen(
onChangeServerSuccess = {
navigator.popBackStack()
}
)
}
@RootNavGraph(start = true)
@Destination
@Composable

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

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

202
features/login/src/main/java/io/element/android/x/features/login/LoginScreen.kt

@ -2,132 +2,184 @@ @@ -2,132 +2,184 @@
package io.element.android.x.features.login
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
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.designsystem.components.VectorButton
import io.element.android.x.designsystem.ElementXTheme
@Composable
fun LoginScreen(
viewModel: LoginViewModel = mavericksViewModel(),
onLoginWithSuccess: () -> Unit = { }
homeserver: String,
onChangeServer: () -> Unit = { },
onLoginWithSuccess: () -> Unit = { },
) {
val state: LoginViewState by viewModel.collectAsState()
LaunchedEffect(key1 = Unit) {
viewModel.homeserver = homeserver
}
LoginContent(
state = state,
onHomeserverChanged = { viewModel.handle(LoginActions.SetHomeserver(it)) },
onLoginChanged = { viewModel.handle(LoginActions.SetLogin(it)) },
onPasswordChanged = { viewModel.handle(LoginActions.SetPassword(it)) },
onSubmitClicked = { viewModel.handle(LoginActions.Submit) },
homeserver = homeserver,
onChangeServer = onChangeServer,
onLoginChanged = viewModel::onSetName,
onPasswordChanged = viewModel::onSetPassword,
onSubmitClicked = viewModel::onSubmit,
onLoginWithSuccess = onLoginWithSuccess
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun LoginContent(
state: LoginViewState,
onHomeserverChanged: (String) -> Unit,
onLoginChanged: (String) -> Unit,
onPasswordChanged: (String) -> Unit,
onSubmitClicked: () -> Unit,
onLoginWithSuccess: () -> Unit
homeserver: String = "",
onChangeServer: () -> Unit = {},
onLoginChanged: (String) -> Unit = {},
onPasswordChanged: (String) -> Unit = {},
onSubmitClicked: () -> Unit = {},
onLoginWithSuccess: () -> Unit = {},
) {
Surface(color = MaterialTheme.colorScheme.background) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
) {
val scrollState = rememberScrollState()
Column(
modifier = Modifier.fillMaxSize(),
modifier = Modifier
.verticalScroll(
state = scrollState,
)
.padding(horizontal = 16.dp),
) {
val isError = state.isLoggedIn is Fail
Image(
painterResource(id = R.drawable.element_logo_green),
contentDescription = null,
modifier = Modifier
.align(Alignment.CenterHorizontally)
.padding(40.dp)
)
OutlinedTextField(
value = state.homeserver,
modifier = Modifier.fillMaxWidth(),
onValueChange = {
onHomeserverChanged(it)
},
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Uri,
),
)
OutlinedTextField(
value = state.login,
// Title
Text(
text = "Welcome back",
modifier = Modifier
.fillMaxWidth()
.padding(top = 8.dp),
onValueChange = {
onLoginChanged(it)
},
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Text,
),
.padding(horizontal = 16.dp, vertical = 48.dp),
textAlign = TextAlign.Center,
fontWeight = FontWeight.Bold,
fontSize = 24.sp,
)
OutlinedTextField(
value = state.password,
modifier = Modifier
.fillMaxWidth()
.padding(top = 8.dp),
onValueChange = {
onPasswordChanged(it)
},
isError = isError,
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Password,
imeAction = ImeAction.Send,
),
)
if (isError) {
Text(
text = (state.isLoggedIn as? Fail)?.toString().orEmpty(),
color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(start = 16.dp)
// Form
Column(
//modifier = Modifier.weight(1f),
) {
Box(
modifier = Modifier.fillMaxWidth()
) {
OutlinedTextField(
value = homeserver,
modifier = Modifier.fillMaxWidth(),
onValueChange = { /* no op */ },
enabled = false,
label = {
Text(text = "Server")
},
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Uri,
),
)
Button(
onClick = onChangeServer,
modifier = Modifier
.align(Alignment.CenterEnd)
.padding(top = 8.dp, end = 8.dp),
content = {
Text(text = "Change")
}
)
}
OutlinedTextField(
value = state.login,
modifier = Modifier
.fillMaxWidth()
.padding(top = 60.dp),
label = {
Text(text = "Email or username")
},
onValueChange = onLoginChanged,
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Text,
),
)
OutlinedTextField(
value = state.password,
modifier = Modifier
.fillMaxWidth()
.padding(top = 24.dp),
onValueChange = onPasswordChanged,
label = {
Text(text = "Password")
},
isError = isError,
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Password,
imeAction = ImeAction.Send,
),
)
if (isError) {
Text(
text = (state.isLoggedIn as? Fail)?.toString().orEmpty(),
color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(start = 16.dp)
)
}
}
VectorButton(
text = "Submit",
onClick = {
onSubmitClicked()
},
// Submit
Button(
onClick = onSubmitClicked,
enabled = state.submitEnabled,
modifier = Modifier
.align(Alignment.End)
.padding(top = 16.dp)
)
if (state.isLoggedIn is Loading) {
// FIXME This does not work, we never enter this if block
CircularProgressIndicator(
modifier = Modifier.align(Alignment.CenterHorizontally)
)
.fillMaxWidth()
.padding(vertical = 32.dp)
) {
Text(text = "Continue")
}
if (state.isLoggedIn is Success) {
onLoginWithSuccess()
}
}
if (state.isLoggedIn is Loading) {
CircularProgressIndicator(
modifier = Modifier.align(Alignment.Center)
)
}
}
}
}
}
@Composable
@Preview
private fun LoginContentPreview() {
ElementXTheme(darkTheme = false) {
LoginContent(
state = LoginViewState(),
homeserver = "matrix.org",
)
}
}

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

@ -1,27 +1,28 @@ @@ -1,27 +1,28 @@
package io.element.android.x.features.login
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.MavericksViewModel
import com.airbnb.mvrx.Uninitialized
import io.element.android.x.matrix.MatrixInstance
import kotlinx.coroutines.launch
class LoginViewModel(initialState: LoginViewState) :
MavericksViewModel<LoginViewState>(initialState) {
lateinit var homeserver: String
private val matrix = MatrixInstance.getInstance()
fun handle(action: LoginActions) {
when (action) {
is LoginActions.SetHomeserver -> handleSetHomeserver(action)
is LoginActions.SetLogin -> handleSetName(action)
is LoginActions.SetPassword -> handleSetPassword(action)
LoginActions.Submit -> handleSubmit()
fun onSubmit() = withState { state ->
setState {
copy(isLoggedIn = Loading())
}
}
private fun handleSubmit() = withState { state ->
viewModelScope.launch {
suspend {
matrix.login(state.homeserver, state.login, state.password)
// Ensure the server is passed to the Rust SDK
matrix.setHomeserver(homeserver)
matrix.login(state.login, state.password)
matrix.activeClient().startSync()
}.execute {
copy(isLoggedIn = it)
@ -29,15 +30,21 @@ class LoginViewModel(initialState: LoginViewState) : @@ -29,15 +30,21 @@ class LoginViewModel(initialState: LoginViewState) :
}
}
private fun handleSetHomeserver(action: LoginActions.SetHomeserver) {
setState { copy(homeserver = action.homeserver) }
}
private fun handleSetPassword(action: LoginActions.SetPassword) {
setState { copy(password = action.password) }
fun onSetPassword(password: String) {
setState {
copy(
password = password,
isLoggedIn = Uninitialized,
)
}
}
private fun handleSetName(action: LoginActions.SetLogin) {
setState { copy(login = action.login) }
fun onSetName(name: String) {
setState {
copy(
login = name,
isLoggedIn = Uninitialized,
)
}
}
}

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

@ -1,17 +1,14 @@ @@ -1,17 +1,14 @@
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
data class LoginViewState(
val homeserver: String = "matrix.org",
val login: String = "",
val password: String = "",
val isLoggedIn: Async<Unit> = Uninitialized,
) : MavericksState {
val submitEnabled = homeserver.isNotEmpty() && login.isNotEmpty() && password.isNotEmpty()
val submitEnabled = login.isNotEmpty() && password.isNotEmpty() && isLoggedIn !is Loading
}

149
features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerScreen.kt

@ -0,0 +1,149 @@ @@ -0,0 +1,149 @@
@file:OptIn(ExperimentalMaterial3Api::class)
package io.element.android.x.features.login.changeserver
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
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
@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,
onChangeServer: (String) -> Unit = {},
onChangeServerSubmit: () -> Unit = {},
onChangeServerSuccess: () -> Unit = {},
) {
Surface(color = MaterialTheme.colorScheme.background) {
Box(modifier = Modifier.fillMaxSize()) {
val scrollState = rememberScrollState()
Column(
modifier = Modifier
.verticalScroll(
state = scrollState,
)
.padding(horizontal = 16.dp),
)
{
val isError = state.changeServerAction is Fail
Text(
modifier = Modifier
.padding(top = 99.dp)
.size(width = 81.dp, height = 73.dp)
.align(Alignment.CenterHorizontally)
.background(
color = MaterialTheme.colorScheme.surfaceVariant,
shape = RoundedCornerShape(32.dp)
),
text = "\uDBC2\uDEAC",
fontSize = 34.sp,
textAlign = TextAlign.Center,
)
Text(
text = "Your server",
modifier = Modifier
.fillMaxWidth()
.defaultMinSize(minHeight = 56.dp)
.align(Alignment.CenterHorizontally)
.padding(top = 38.dp),
textAlign = TextAlign.Center,
fontWeight = FontWeight.Bold,
fontSize = 24.sp,
)
Text(
text = "A server is a home for all your data.\n" +
"You choose your server and it’s easy to make one.", // TODO "Learn more.",
modifier = Modifier
.fillMaxWidth()
.align(Alignment.CenterHorizontally)
.padding(top = 16.dp),
textAlign = TextAlign.Center,
fontSize = 16.sp,
color = MaterialTheme.colorScheme.secondary
)
OutlinedTextField(
value = state.homeserver,
modifier = Modifier
.fillMaxWidth()
.padding(top = 200.dp),
onValueChange = onChangeServer,
label = {
Text(text = "Server")
},
isError = isError,
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Password,
imeAction = ImeAction.Send,
),
)
if (isError) {
Text(
text = (state.changeServerAction as? Fail)?.toString().orEmpty(),
color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(start = 16.dp)
)
}
Button(
onClick = onChangeServerSubmit,
enabled = state.submitEnabled,
modifier = Modifier
.fillMaxWidth()
.padding(top = 44.dp)
) {
Text(text = "Continue")
}
if (state.changeServerAction is Success) {
onChangeServerSuccess()
}
}
if (state.changeServerAction is Loading) {
CircularProgressIndicator(
modifier = Modifier.align(Alignment.Center)
)
}
}
}
}
@Composable
@Preview
private fun ChangeServerContentPreview() {
ChangeServerContent(
state = ChangeServerViewState(homeserver = "matrix.org"),
)
}

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

@ -0,0 +1,32 @@ @@ -0,0 +1,32 @@
package io.element.android.x.features.login.changeserver
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.MavericksViewModel
import io.element.android.x.matrix.MatrixInstance
import kotlinx.coroutines.launch
class ChangeServerViewModel(initialState: ChangeServerViewState) :
MavericksViewModel<ChangeServerViewState>(initialState) {
private val matrix = MatrixInstance.getInstance()
fun setServer(server: String) {
setState {
copy(homeserver = server)
}
}
fun setServerSubmit() = withState { state ->
setState {
copy(changeServerAction = Loading())
}
viewModelScope.launch {
suspend {
matrix.setHomeserver(state.homeserver)
}.execute { it ->
copy(changeServerAction = it)
}
}
}
}

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

@ -0,0 +1,13 @@ @@ -0,0 +1,13 @@
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 = "matrix.org",
val changeServerAction: Async<Unit> = Uninitialized,
) : MavericksState {
val submitEnabled = homeserver.isNotEmpty() && changeServerAction !is Loading
}

5
features/onboarding/src/main/java/io/element/android/x/features/onboarding/OnBoardingActions.kt

@ -1,5 +0,0 @@ @@ -1,5 +0,0 @@
package io.element.android.x.features.onboarding
sealed interface OnBoardingActions {
data class GoToPage(val page: Int) : OnBoardingActions
}

1
libraries/core/build.gradle

@ -48,4 +48,5 @@ dependencies { @@ -48,4 +48,5 @@ dependencies {
implementation 'androidx.fragment:fragment-ktx:1.5.3'
implementation 'com.airbnb.android:mavericks-compose:3.0.1'
implementation 'androidx.compose.foundation:foundation-layout:1.3.1'
}

26
libraries/core/src/main/java/io/element/android/x/core/compose/Keyboard.kt

@ -0,0 +1,26 @@ @@ -0,0 +1,26 @@
package io.element.android.x.core.compose
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.isImeVisible
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.lifecycle.Lifecycle
/**
* Inspired from https://stackoverflow.com/questions/68847559/how-can-i-detect-keyboard-opening-and-closing-in-jetpack-compose
*/
enum class Keyboard {
Opened, Closed
}
// Note: it does not work as expected...
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun keyboardAsState(): State<Keyboard> {
val lifecycle = LocalLifecycleOwner.current.lifecycle
val isResumed = lifecycle.currentState == Lifecycle.State.RESUMED
return rememberUpdatedState(if (WindowInsets.isImeVisible && isResumed) Keyboard.Opened else Keyboard.Closed)
}

10
libraries/matrix/src/main/java/io/element/android/x/matrix/Matrix.kt

@ -7,7 +7,6 @@ import io.element.android.x.matrix.util.logError @@ -7,7 +7,6 @@ import io.element.android.x.matrix.util.logError
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.AuthenticationService
import org.matrix.rustcomponents.sdk.Client
@ -27,6 +26,7 @@ class Matrix( @@ -27,6 +26,7 @@ class Matrix(
private val baseFolder = File(context.filesDir, "matrix")
private val sessionStore = SessionStore(context)
private val matrixClient = MutableStateFlow<Optional<MatrixClient>>(Optional.empty())
private val authService = AuthenticationService(baseFolder.absolutePath)
init {
sessionStore.isLoggedIn()
@ -70,10 +70,14 @@ class Matrix( @@ -70,10 +70,14 @@ class Matrix(
}
}
suspend fun login(homeserver: String, username: String, password: String): MatrixClient =
suspend fun setHomeserver(homeserver: String) {
withContext(coroutineDispatchers.io) {
val authService = AuthenticationService(baseFolder.absolutePath)
authService.configureHomeserver(homeserver)
}
}
suspend fun login(username: String, password: String): MatrixClient =
withContext(coroutineDispatchers.io) {
val client = authService.login(username, password, "MatrixRustSDKSample", null)
sessionStore.storeData(client.session())
createMatrixClient(client)

Loading…
Cancel
Save