From 4ce0b62241360d5ab08120f894d7f75a7a98b095 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 11 Oct 2022 16:18:42 +0200 Subject: [PATCH] Introduce mavericks-compose and room list module - WIP --- app/build.gradle | 3 + .../element/android/x/ElementXApplication.kt | 2 + .../java/io/element/android/x/MainActivity.kt | 18 ++++- libraries/core/build.gradle | 51 +++++++++++++ libraries/core/src/main/AndroidManifest.xml | 2 + libraries/ui/screens/login/build.gradle | 3 + .../x/ui/screen/login/LoginActivity.kt | 47 +++++++++--- .../x/ui/screen/login/LoginViewModel.kt | 72 +++++++++++-------- .../x/ui/screen/login/LoginViewState.kt | 7 +- libraries/ui/screens/roomlist/build.gradle | 61 ++++++++++++++++ .../roomlist/src/main/AndroidManifest.xml | 8 +++ .../x/ui/screen/login/RoomListActions.kt | 5 ++ .../x/ui/screen/login/RoomListActivity.kt | 47 ++++++++++++ .../x/ui/screen/login/RoomListViewModel.kt | 28 ++++++++ .../x/ui/screen/login/RoomListViewState.kt | 6 ++ .../x/ui/theme/components/VectorButton.kt | 4 +- .../x/ui/theme/components/VectorTextField.kt | 5 +- settings.gradle | 2 + 18 files changed, 328 insertions(+), 43 deletions(-) create mode 100644 libraries/core/build.gradle create mode 100644 libraries/core/src/main/AndroidManifest.xml create mode 100644 libraries/ui/screens/roomlist/build.gradle create mode 100644 libraries/ui/screens/roomlist/src/main/AndroidManifest.xml create mode 100644 libraries/ui/screens/roomlist/src/main/java/io/element/android/x/ui/screen/login/RoomListActions.kt create mode 100644 libraries/ui/screens/roomlist/src/main/java/io/element/android/x/ui/screen/login/RoomListActivity.kt create mode 100644 libraries/ui/screens/roomlist/src/main/java/io/element/android/x/ui/screen/login/RoomListViewModel.kt create mode 100644 libraries/ui/screens/roomlist/src/main/java/io/element/android/x/ui/screen/login/RoomListViewState.kt diff --git a/app/build.gradle b/app/build.gradle index d4d4b98158..71c9701f6b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -57,6 +57,7 @@ android { dependencies { implementation project(":libraries:ui:theme") implementation project(":libraries:ui:screens:login") + implementation project(":libraries:ui:screens:roomlist") implementation project(":libraries:sdk:matrix") implementation 'androidx.core:core-ktx:1.9.0' @@ -67,4 +68,6 @@ dependencies { implementation 'androidx.activity:activity-compose:1.6.0' debugImplementation "androidx.compose.ui:ui-tooling:$compose_version" debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version" + + implementation 'com.airbnb.android:mavericks-compose:2.7.0' } \ No newline at end of file diff --git a/app/src/main/java/io/element/android/x/ElementXApplication.kt b/app/src/main/java/io/element/android/x/ElementXApplication.kt index 6ed4dfc1c1..88136d002b 100644 --- a/app/src/main/java/io/element/android/x/ElementXApplication.kt +++ b/app/src/main/java/io/element/android/x/ElementXApplication.kt @@ -1,6 +1,7 @@ package io.element.android.x import android.app.Application +import com.airbnb.mvrx.Mavericks import io.element.android.x.sdk.matrix.MatrixInstance class ElementXApplication : Application() { @@ -8,5 +9,6 @@ class ElementXApplication : Application() { override fun onCreate() { super.onCreate() MatrixInstance.init(this) + Mavericks.initialize(this) } } \ No newline at end of file diff --git a/app/src/main/java/io/element/android/x/MainActivity.kt b/app/src/main/java/io/element/android/x/MainActivity.kt index 83797b5271..d9097dca77 100644 --- a/app/src/main/java/io/element/android/x/MainActivity.kt +++ b/app/src/main/java/io/element/android/x/MainActivity.kt @@ -3,14 +3,30 @@ package io.element.android.x import android.content.Intent import android.os.Bundle import androidx.activity.ComponentActivity +import androidx.activity.result.contract.ActivityResultContracts import io.element.android.x.ui.screen.login.LoginActivity +import io.element.android.x.ui.screen.login.RoomListActivity class MainActivity : ComponentActivity() { + private val launcher = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + if (it.resultCode == RESULT_OK) { + // Launch the room Activity and finish + startRoomActivityAndFinish() + } else { + finish() + } + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // Just start the LoginActivity for now. // TODO if a session exist, start the room list - startActivity(Intent(this, LoginActivity::class.java)) + launcher.launch(Intent(this, LoginActivity::class.java)) + } + + private fun startRoomActivityAndFinish() { + startActivity(Intent(this, RoomListActivity::class.java)) finish() } } diff --git a/libraries/core/build.gradle b/libraries/core/build.gradle new file mode 100644 index 0000000000..b1ec9bdb80 --- /dev/null +++ b/libraries/core/build.gradle @@ -0,0 +1,51 @@ +plugins { + id 'com.android.library' + id 'org.jetbrains.kotlin.android' +} + +android { + namespace 'io.element.android.x.core' + compileSdk 33 + + defaultConfig { + minSdk 29 + targetSdk 33 + + consumerProguardFiles "consumer-rules.pro" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + buildFeatures { + compose true + } + composeOptions { + kotlinCompilerExtensionVersion compose_version + } + kotlinOptions { + jvmTarget = '1.8' + } +} + +dependencies { + implementation 'androidx.core:core-ktx:1.9.0' + implementation 'androidx.appcompat:appcompat:1.5.1' + + implementation 'com.google.android.material:material:1.6.1' + + implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1' + implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1" + implementation 'androidx.activity:activity-compose:1.6.0' + + implementation 'androidx.fragment:fragment-ktx:1.5.3' + + implementation 'com.airbnb.android:mavericks-compose:2.7.0' +} \ No newline at end of file diff --git a/libraries/core/src/main/AndroidManifest.xml b/libraries/core/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..568741e54f --- /dev/null +++ b/libraries/core/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/libraries/ui/screens/login/build.gradle b/libraries/ui/screens/login/build.gradle index 012c0f8161..14552fbb08 100644 --- a/libraries/ui/screens/login/build.gradle +++ b/libraries/ui/screens/login/build.gradle @@ -36,6 +36,7 @@ android { } dependencies { + implementation project(":libraries:core") implementation project(":libraries:ui:theme") implementation project(":libraries:sdk:matrix") @@ -55,4 +56,6 @@ dependencies { implementation 'androidx.activity:activity-compose:1.6.0' implementation 'androidx.fragment:fragment-ktx:1.5.3' + + implementation 'com.airbnb.android:mavericks-compose:2.7.0' } \ No newline at end of file diff --git a/libraries/ui/screens/login/src/main/java/io/element/android/x/ui/screen/login/LoginActivity.kt b/libraries/ui/screens/login/src/main/java/io/element/android/x/ui/screen/login/LoginActivity.kt index 95c1b84a83..919db9c71c 100644 --- a/libraries/ui/screens/login/src/main/java/io/element/android/x/ui/screen/login/LoginActivity.kt +++ b/libraries/ui/screens/login/src/main/java/io/element/android/x/ui/screen/login/LoginActivity.kt @@ -1,25 +1,31 @@ package io.element.android.x.ui.screen.login +import android.app.Activity import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent -import androidx.activity.viewModels import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface -import androidx.compose.runtime.collectAsState +import androidx.compose.material3.Text +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +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.ui.theme.ElementXTheme import io.element.android.x.ui.theme.components.VectorButton import io.element.android.x.ui.theme.components.VectorTextField class LoginActivity : ComponentActivity() { - private val viewModel: LoginViewModel by viewModels() - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -35,9 +41,10 @@ class LoginActivity : ComponentActivity() { Column( modifier = Modifier.fillMaxSize() ) { - val state = viewModel.state.collectAsState().value - VectorTextField( - value = state.homeserver, + val viewModel: LoginViewModel = mavericksViewModel() + val state by viewModel.collectAsState() + val isError = state.isLoggedIn is Fail + VectorTextField(value = state.homeserver, onValueChange = { viewModel.handle(LoginActions.SetHomeserver(it)) }) @@ -50,18 +57,42 @@ class LoginActivity : ComponentActivity() { value = state.password, onValueChange = { viewModel.handle(LoginActions.SetPassword(it)) - } + }, + isError = isError ) + 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 = { viewModel.handle(LoginActions.Submit) }, enabled = state.submitEnabled, + modifier = Modifier.align(Alignment.End) ) + if (state.isLoggedIn is Loading) { + // FIXME This does not work, we never enter this if block + CircularProgressIndicator( + modifier = Modifier.align(Alignment.CenterHorizontally) + ) + } + if (state.isLoggedIn is Success) { + openRoomList() + } } } } } } + + private fun openRoomList() { + setResult(Activity.RESULT_OK) + finish() + } } diff --git a/libraries/ui/screens/login/src/main/java/io/element/android/x/ui/screen/login/LoginViewModel.kt b/libraries/ui/screens/login/src/main/java/io/element/android/x/ui/screen/login/LoginViewModel.kt index 2a8812e53a..efb486ca5e 100644 --- a/libraries/ui/screens/login/src/main/java/io/element/android/x/ui/screen/login/LoginViewModel.kt +++ b/libraries/ui/screens/login/src/main/java/io/element/android/x/ui/screen/login/LoginViewModel.kt @@ -1,26 +1,38 @@ package io.element.android.x.ui.screen.login import android.util.Log -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.MavericksViewModel +import com.airbnb.mvrx.Success import io.element.android.x.sdk.matrix.MatrixInstance -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch -class LoginViewModel : ViewModel() { +class LoginViewModel(initialState: LoginViewState) : + MavericksViewModel(initialState) { private val matrix = MatrixInstance.getInstance() - private val _state = MutableStateFlow(LoginViewState()) - val state = _state.asStateFlow() - init { observeState() } private fun observeState() { - // TODO Update submitEnabled when other state members are updated. + onEach( + LoginViewState::homeserver, + LoginViewState::login, + LoginViewState::password, + LoginViewState::isLoggedIn, + ) { homeserver, login, password, isLoggedIn -> + setState { + copy( + submitEnabled = homeserver.isNotEmpty() && + login.isNotEmpty() && + password.isNotEmpty() && + isLoggedIn !is Loading + ) + } + } } fun handle(action: LoginActions) { @@ -33,40 +45,40 @@ class LoginViewModel : ViewModel() { } private fun handleSetHomeserver(action: LoginActions.SetHomeserver) { - _state.value = _state.value.copy( - homeserver = action.homeserver, - submitEnabled = _state.value.login.isNotEmpty() && - _state.value.password.isNotEmpty() && - action.homeserver.isNotEmpty() - ) + setState { + copy( + homeserver = action.homeserver + ) + } } - private fun handleSubmit() { + private fun handleSubmit() = withState { state -> viewModelScope.launch { - val currentState = state.value + setState { copy(isLoggedIn = Loading()) } try { - matrix.login(currentState.homeserver, currentState.login, currentState.password) + matrix.login(state.homeserver, state.login, state.password) + setState { copy(isLoggedIn = Success(Unit)) } } catch (throwable: Throwable) { Log.e("Error", "Cannot login", throwable) + setState { copy(isLoggedIn = Fail(throwable)) } } } } private fun handleSetPassword(action: LoginActions.SetPassword) { - _state.value = _state.value.copy( - password = action.password, - submitEnabled = _state.value.login.isNotEmpty() && - _state.value.homeserver.isNotEmpty() && - action.password.isNotEmpty() - ) + setState { + copy( + password = action.password + ) + } } private fun handleSetName(action: LoginActions.SetLogin) { - _state.value = _state.value.copy( - login = action.login, - submitEnabled = action.login.isNotEmpty() && - _state.value.homeserver.isNotEmpty() && - _state.value.password.isNotEmpty() - ) + setState { + copy( + + login = action.login + ) + } } } \ No newline at end of file diff --git a/libraries/ui/screens/login/src/main/java/io/element/android/x/ui/screen/login/LoginViewState.kt b/libraries/ui/screens/login/src/main/java/io/element/android/x/ui/screen/login/LoginViewState.kt index 406fc55751..83c9ec295a 100644 --- a/libraries/ui/screens/login/src/main/java/io/element/android/x/ui/screen/login/LoginViewState.kt +++ b/libraries/ui/screens/login/src/main/java/io/element/android/x/ui/screen/login/LoginViewState.kt @@ -1,8 +1,13 @@ package io.element.android.x.ui.screen.login +import com.airbnb.mvrx.Async +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 submitEnabled: Boolean = false, -) + val isLoggedIn: Async = Uninitialized, +) : MavericksState diff --git a/libraries/ui/screens/roomlist/build.gradle b/libraries/ui/screens/roomlist/build.gradle new file mode 100644 index 0000000000..3b9f08b932 --- /dev/null +++ b/libraries/ui/screens/roomlist/build.gradle @@ -0,0 +1,61 @@ +plugins { + id 'com.android.library' + id 'org.jetbrains.kotlin.android' +} + +android { + namespace 'io.element.android.x.ui.screen.roomlist' + compileSdk 33 + + defaultConfig { + minSdk 29 + targetSdk 33 + + consumerProguardFiles "consumer-rules.pro" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + buildFeatures { + compose true + } + composeOptions { + kotlinCompilerExtensionVersion compose_version + } + kotlinOptions { + jvmTarget = '1.8' + } +} + +dependencies { + implementation project(":libraries:core") + implementation project(":libraries:ui:theme") + implementation project(":libraries:sdk:matrix") + + implementation 'androidx.core:core-ktx:1.9.0' + implementation 'androidx.appcompat:appcompat:1.5.1' + + implementation 'com.google.android.material:material:1.6.1' + + implementation "androidx.compose.ui:ui:$compose_version" + implementation 'androidx.compose.material3:material3:1.0.0-rc01' + implementation "androidx.compose.ui:ui-tooling-preview:$compose_version" + debugImplementation "androidx.compose.ui:ui-tooling:$compose_version" + debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version" + + implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1' + implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1" + implementation 'androidx.activity:activity-compose:1.6.0' + + implementation 'androidx.fragment:fragment-ktx:1.5.3' + + implementation 'com.airbnb.android:mavericks-compose:2.7.0' +} \ No newline at end of file diff --git a/libraries/ui/screens/roomlist/src/main/AndroidManifest.xml b/libraries/ui/screens/roomlist/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..f292776d9a --- /dev/null +++ b/libraries/ui/screens/roomlist/src/main/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/libraries/ui/screens/roomlist/src/main/java/io/element/android/x/ui/screen/login/RoomListActions.kt b/libraries/ui/screens/roomlist/src/main/java/io/element/android/x/ui/screen/login/RoomListActions.kt new file mode 100644 index 0000000000..6655529cf3 --- /dev/null +++ b/libraries/ui/screens/roomlist/src/main/java/io/element/android/x/ui/screen/login/RoomListActions.kt @@ -0,0 +1,5 @@ +package io.element.android.x.ui.screen.login + +sealed interface RoomListActions { + object LoadMore : RoomListActions +} diff --git a/libraries/ui/screens/roomlist/src/main/java/io/element/android/x/ui/screen/login/RoomListActivity.kt b/libraries/ui/screens/roomlist/src/main/java/io/element/android/x/ui/screen/login/RoomListActivity.kt new file mode 100644 index 0000000000..12146e32b1 --- /dev/null +++ b/libraries/ui/screens/roomlist/src/main/java/io/element/android/x/ui/screen/login/RoomListActivity.kt @@ -0,0 +1,47 @@ +package io.element.android.x.ui.screen.login + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.viewModels +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.runtime.collectAsState +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import io.element.android.x.ui.theme.ElementXTheme + +class RoomListActivity : ComponentActivity() { + + private val viewModel: RoomListViewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContent { + ElementXTheme { + // A surface container using the 'background' color from the theme + Surface( + modifier = Modifier + .fillMaxSize() + .padding(16.dp), + color = MaterialTheme.colorScheme.background + ) { + Column( + modifier = Modifier.fillMaxSize() + ) { + /* TODO + val state = viewModel.state.collectAsState().value + RoomListHeader() + RoomList() + + */ + } + } + } + } + } +} diff --git a/libraries/ui/screens/roomlist/src/main/java/io/element/android/x/ui/screen/login/RoomListViewModel.kt b/libraries/ui/screens/roomlist/src/main/java/io/element/android/x/ui/screen/login/RoomListViewModel.kt new file mode 100644 index 0000000000..aa3634b94c --- /dev/null +++ b/libraries/ui/screens/roomlist/src/main/java/io/element/android/x/ui/screen/login/RoomListViewModel.kt @@ -0,0 +1,28 @@ +package io.element.android.x.ui.screen.login + +import androidx.lifecycle.ViewModel +import io.element.android.x.sdk.matrix.MatrixInstance +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow + +class RoomListViewModel : ViewModel() { + + private val matrix = MatrixInstance.getInstance() + + private val _state = MutableStateFlow(RoomListViewState()) + val state = _state.asStateFlow() + + init { + observeState() + } + + private fun observeState() { + // TODO Update submitEnabled when other state members are updated. + } + + fun handle(action: RoomListActions) { + when (action) { + RoomListActions.LoadMore -> TODO() + } + } +} \ No newline at end of file diff --git a/libraries/ui/screens/roomlist/src/main/java/io/element/android/x/ui/screen/login/RoomListViewState.kt b/libraries/ui/screens/roomlist/src/main/java/io/element/android/x/ui/screen/login/RoomListViewState.kt new file mode 100644 index 0000000000..7ed1906b89 --- /dev/null +++ b/libraries/ui/screens/roomlist/src/main/java/io/element/android/x/ui/screen/login/RoomListViewState.kt @@ -0,0 +1,6 @@ +package io.element.android.x.ui.screen.login + +data class RoomListViewState( + val list: List = emptyList(), + val canLoadMore: Boolean = false, +) diff --git a/libraries/ui/theme/src/main/java/io/element/android/x/ui/theme/components/VectorButton.kt b/libraries/ui/theme/src/main/java/io/element/android/x/ui/theme/components/VectorButton.kt index 6a237fc857..03413a9b72 100644 --- a/libraries/ui/theme/src/main/java/io/element/android/x/ui/theme/components/VectorButton.kt +++ b/libraries/ui/theme/src/main/java/io/element/android/x/ui/theme/components/VectorButton.kt @@ -3,13 +3,15 @@ package io.element.android.x.ui.theme.components import androidx.compose.material3.Button import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier @Composable -fun VectorButton(text: String, enabled: Boolean, onClick: () -> Unit) { +fun VectorButton(text: String, enabled: Boolean, onClick: () -> Unit, modifier: Modifier? = null) { Button( onClick = onClick, enabled = enabled, + modifier = modifier ?: Modifier ) { Text(text = text) } diff --git a/libraries/ui/theme/src/main/java/io/element/android/x/ui/theme/components/VectorTextField.kt b/libraries/ui/theme/src/main/java/io/element/android/x/ui/theme/components/VectorTextField.kt index a46e61a1b5..7d2b61e734 100644 --- a/libraries/ui/theme/src/main/java/io/element/android/x/ui/theme/components/VectorTextField.kt +++ b/libraries/ui/theme/src/main/java/io/element/android/x/ui/theme/components/VectorTextField.kt @@ -9,10 +9,11 @@ import androidx.compose.ui.Modifier @OptIn(ExperimentalMaterial3Api::class) @Composable -fun VectorTextField(value: String, onValueChange: (String) -> Unit) { +fun VectorTextField(value: String, onValueChange: (String) -> Unit, isError: Boolean = false) { OutlinedTextField( value = value, onValueChange = onValueChange, - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxWidth(), + isError = isError ) } diff --git a/settings.gradle b/settings.gradle index 55bf47e307..aa27129117 100644 --- a/settings.gradle +++ b/settings.gradle @@ -17,6 +17,8 @@ dependencyResolutionManagement { } rootProject.name = "ElementX" include ':app' +include ':libraries:core' include ':libraries:ui:theme' include ':libraries:ui:screens:login' +include ':libraries:ui:screens:roomlist' include ':libraries:sdk:matrix'