Merge remote-tracking branch 'origin/develop' into develop

This commit is contained in:
Benoit Marty 2022-10-28 17:27:40 +02:00
commit a2f0b1c541
95 changed files with 931 additions and 595 deletions

View file

@ -0,0 +1,29 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
</profile>
</component>

25
.idea/jarRepositories.xml generated Normal file
View file

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteRepositoriesConfiguration">
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Maven Central repository" />
<option name="url" value="https://repo1.maven.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="jboss.community" />
<option name="name" value="JBoss Community repository" />
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
</remote-repository>
<remote-repository>
<option name="id" value="MavenRepo" />
<option name="name" value="MavenRepo" />
<option name="url" value="https://repo.maven.apache.org/maven2/" />
</remote-repository>
<remote-repository>
<option name="id" value="Google" />
<option name="name" value="Google" />
<option name="url" value="https://dl.google.com/dl/android/maven2/" />
</remote-repository>
</component>
</project>

3
.idea/misc.xml generated
View file

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="Android Studio default JDK" project-jdk-type="JavaSDK"> <component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="11" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" /> <output url="file://$PROJECT_DIR$/build/classes" />
</component> </component>
<component name="ProjectType"> <component name="ProjectType">

View file

@ -1,6 +1,7 @@
plugins { plugins {
id 'com.android.application' id 'com.android.application'
id 'org.jetbrains.kotlin.android' id 'org.jetbrains.kotlin.android'
id 'com.google.devtools.ksp' version '1.7.20-1.0.7'
} }
android { android {
@ -36,7 +37,7 @@ android {
compose true compose true
} }
composeOptions { composeOptions {
kotlinCompilerExtensionVersion compose_version kotlinCompilerExtensionVersion "1.3.2"
} }
packagingOptions { packagingOptions {
resources { resources {
@ -44,6 +45,14 @@ android {
} }
} }
applicationVariants.all { variant ->
kotlin.sourceSets {
getByName(variant.name) {
kotlin.srcDir("build/generated/ksp/${variant.name}/kotlin")
}
}
}
signingConfigs { signingConfigs {
debug { debug {
keyAlias 'androiddebugkey' keyAlias 'androiddebugkey'
@ -55,19 +64,26 @@ android {
} }
dependencies { dependencies {
implementation project(":libraries:ui:theme") implementation project(":libraries:designsystem")
implementation project(":libraries:ui:screens:login") implementation project(":libraries:matrix")
implementation project(":libraries:ui:screens:roomlist") implementation project(":features:login")
implementation project(":libraries:sdk:matrix") implementation project(":features:roomlist")
implementation 'io.github.raamcosta.compose-destinations:core:1.7.23-beta'
ksp 'io.github.raamcosta.compose-destinations:ksp:1.7.23-beta'
def composeBom = platform('androidx.compose:compose-bom:2022.10.00')
implementation composeBom
androidTestImplementation composeBom
implementation 'androidx.core:core-ktx:1.9.0' implementation 'androidx.core:core-ktx:1.9.0'
implementation "androidx.compose.ui:ui:$compose_version" implementation "androidx.compose.ui:ui"
implementation 'androidx.compose.material3:material3:1.0.0-rc01' implementation 'androidx.compose.material3:material3'
implementation "androidx.compose.ui:ui-tooling-preview:$compose_version" implementation "androidx.compose.ui:ui-tooling-preview"
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1' implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1'
implementation 'androidx.activity:activity-compose:1.6.0' implementation 'androidx.activity:activity-compose:1.6.1'
debugImplementation "androidx.compose.ui:ui-tooling:$compose_version" implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1'
debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version" debugImplementation "androidx.compose.ui:ui-tooling"
debugImplementation "androidx.compose.ui:ui-test-manifest"
implementation 'com.airbnb.android:mavericks-compose:2.7.0' implementation 'com.airbnb.android:mavericks-compose:3.0.1'
} }

View file

@ -2,7 +2,7 @@ package io.element.android.x
import android.app.Application import android.app.Application
import com.airbnb.mvrx.Mavericks import com.airbnb.mvrx.Mavericks
import io.element.android.x.sdk.matrix.MatrixInstance import io.element.android.x.matrix.MatrixInstance
class ElementXApplication : Application() { class ElementXApplication : Application() {

View file

@ -1,41 +1,41 @@
package io.element.android.x package io.element.android.x
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.compose.setContent
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.lifecycle.lifecycleScope import androidx.compose.runtime.Composable
import io.element.android.x.ui.screen.login.LoginActivity import com.ramcosta.composedestinations.DestinationsNavHost
import io.element.android.x.ui.screen.roomlist.RoomListActivity import com.ramcosta.composedestinations.rememberNavHostEngine
import kotlinx.coroutines.launch import io.element.android.x.designsystem.ElementXTheme
import io.element.android.x.destinations.LoginScreenNavigationDestination
import kotlinx.coroutines.runBlocking
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
private val launcher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == RESULT_OK) {
// Launch the room Activity and finish
startRoomActivityAndFinish()
} else {
finish()
}
}
private val viewModel: MainViewModel by viewModels() private val viewModel: MainViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
lifecycleScope.launch { setContent {
if (viewModel.hasSession()) { ElementXTheme {
startRoomActivityAndFinish() MainScreen(viewModel = viewModel)
} else {
launcher.launch(Intent(this@MainActivity, LoginActivity::class.java))
} }
} }
} }
}
private fun startRoomActivityAndFinish() {
startActivity(Intent(this, RoomListActivity::class.java)) @Composable
finish() private fun MainScreen(viewModel: MainViewModel) {
} val engine = rememberNavHostEngine()
val navController = engine.rememberNavController()
val startRoute = runBlocking {
if (!viewModel.hasSession()) LoginScreenNavigationDestination else NavGraphs.root.startRoute
}
DestinationsNavHost(
engine = engine,
navController = navController,
navGraph = NavGraphs.root,
startRoute = startRoute
)
} }

View file

@ -1,7 +1,7 @@
package io.element.android.x package io.element.android.x
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import io.element.android.x.sdk.matrix.MatrixInstance import io.element.android.x.matrix.MatrixInstance
class MainViewModel : ViewModel() { class MainViewModel : ViewModel() {
private val matrix = MatrixInstance.getInstance() private val matrix = MatrixInstance.getInstance()

View file

@ -0,0 +1,28 @@
package io.element.android.x
import androidx.compose.runtime.Composable
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootNavGraph
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import io.element.android.x.destinations.RoomListScreenNavigationDestination
import io.element.android.x.features.login.LoginScreen
import io.element.android.x.features.roomlist.RoomListScreen
@Destination
@Composable
fun LoginScreenNavigation(navigator: DestinationsNavigator) {
LoginScreen(
onLoginWithSuccess = {
navigator.clearBackStack(RoomListScreenNavigationDestination)
}
)
}
@RootNavGraph(start = true)
@Destination
@Composable
fun RoomListScreenNavigation() {
RoomListScreen()
}

View file

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 982 B

After

Width:  |  Height:  |  Size: 982 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

Before After
Before After

View file

@ -1,12 +1,8 @@
buildscript { // Top-level build file where you can add configuration options common to all sub-projects/modules.
ext {
compose_version = '1.3.0-rc01'
}
}// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins { plugins {
id 'com.android.application' version '7.3.0' apply false id 'com.android.application' version '7.3.0' apply false
id 'com.android.library' version '7.3.0' apply false id 'com.android.library' version '7.3.0' apply false
id 'org.jetbrains.kotlin.android' version '1.7.10' apply false id 'org.jetbrains.kotlin.android' version '1.7.20' apply false
} }
task clean(type: Delete) { task clean(type: Delete) {

1
features/login/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,18 @@
plugins {
id("io.element.android-compose")
}
android {
namespace = "io.element.android.x.features.login"
}
dependencies {
implementation(project(":libraries:core"))
implementation(project(":libraries:matrix"))
implementation(project(":libraries:designsystem"))
implementation("com.airbnb.android:mavericks-compose:3.0.1")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.3")
androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0")
}

View file

21
features/login/proguard-rules.pro vendored Normal file
View file

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.kts.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View file

@ -0,0 +1,24 @@
package io.element.android.x.features.login
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("io.element.android.x.features.login.test", appContext.packageName)
}
}

View file

@ -1,4 +1,4 @@
package io.element.android.x.ui.screen.login package io.element.android.x.features.login
sealed interface LoginActions { sealed interface LoginActions {
data class SetHomeserver(val homeserver: String) : LoginActions data class SetHomeserver(val homeserver: String) : LoginActions

View file

@ -0,0 +1,133 @@
@file:OptIn(ExperimentalMaterial3Api::class)
package io.element.android.x.features.login
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.KeyboardOptions
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.res.painterResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
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.designsystem.components.VectorButton
@Composable
fun LoginScreen(
viewModel: LoginViewModel = mavericksViewModel(),
onLoginWithSuccess: () -> Unit = { }
) {
val state: LoginViewState by viewModel.collectAsState()
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) },
onLoginWithSuccess = onLoginWithSuccess
)
}
@Composable
fun LoginContent(
state: LoginViewState,
onHomeserverChanged: (String) -> Unit,
onLoginChanged: (String) -> Unit,
onPasswordChanged: (String) -> Unit,
onSubmitClicked: () -> Unit,
onLoginWithSuccess: () -> Unit
) {
Surface(color = MaterialTheme.colorScheme.background) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
) {
Column(
modifier = Modifier.fillMaxSize(),
) {
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,
modifier = Modifier
.fillMaxWidth()
.padding(top = 8.dp),
onValueChange = {
onLoginChanged(it)
},
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Text,
),
)
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)
)
}
VectorButton(
text = "Submit",
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)
)
}
if (state.isLoggedIn is Success) {
onLoginWithSuccess()
}
}
}
}
}

View file

@ -1,11 +1,11 @@
package io.element.android.x.ui.screen.login package io.element.android.x.features.login
import android.util.Log import android.util.Log
import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.MavericksViewModel import com.airbnb.mvrx.MavericksViewModel
import com.airbnb.mvrx.Success import com.airbnb.mvrx.Success
import io.element.android.x.sdk.matrix.MatrixInstance import io.element.android.x.matrix.MatrixInstance
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class LoginViewModel(initialState: LoginViewState) : class LoginViewModel(initialState: LoginViewState) :
@ -13,28 +13,6 @@ class LoginViewModel(initialState: LoginViewState) :
private val matrix = MatrixInstance.getInstance() private val matrix = MatrixInstance.getInstance()
init {
observeState()
}
private fun observeState() {
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) { fun handle(action: LoginActions) {
when (action) { when (action) {
is LoginActions.SetHomeserver -> handleSetHomeserver(action) is LoginActions.SetHomeserver -> handleSetHomeserver(action)
@ -44,14 +22,6 @@ class LoginViewModel(initialState: LoginViewState) :
} }
} }
private fun handleSetHomeserver(action: LoginActions.SetHomeserver) {
setState {
copy(
homeserver = action.homeserver
)
}
}
private fun handleSubmit() = withState { state -> private fun handleSubmit() = withState { state ->
viewModelScope.launch { viewModelScope.launch {
setState { copy(isLoggedIn = Loading()) } setState { copy(isLoggedIn = Loading()) }
@ -65,20 +35,15 @@ class LoginViewModel(initialState: LoginViewState) :
} }
} }
private fun handleSetHomeserver(action: LoginActions.SetHomeserver) {
setState { copy(homeserver = action.homeserver) }
}
private fun handleSetPassword(action: LoginActions.SetPassword) { private fun handleSetPassword(action: LoginActions.SetPassword) {
setState { setState { copy(password = action.password) }
copy(
password = action.password
)
}
} }
private fun handleSetName(action: LoginActions.SetLogin) { private fun handleSetName(action: LoginActions.SetLogin) {
setState { setState { copy(login = action.login) }
copy(
login = action.login
)
}
} }
} }

View file

@ -1,4 +1,4 @@
package io.element.android.x.ui.screen.login package io.element.android.x.features.login
import com.airbnb.mvrx.Async import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.MavericksState
@ -8,6 +8,10 @@ data class LoginViewState(
val homeserver: String = "matrix.org", val homeserver: String = "matrix.org",
val login: String = "", val login: String = "",
val password: String = "", val password: String = "",
val submitEnabled: Boolean = false,
val isLoggedIn: Async<Unit> = Uninitialized, val isLoggedIn: Async<Unit> = Uninitialized,
) : MavericksState ) : MavericksState {
val submitEnabled = homeserver.isNotEmpty() && login.isNotEmpty() && password.isNotEmpty()
}

View file

@ -0,0 +1,22 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="64dp"
android:height="64dp"
android:viewportWidth="64"
android:viewportHeight="64">
<path
android:pathData="M23.04,3.84C23.04,1.7192 24.7593,0 26.88,0C41.0185,0 52.48,11.4615 52.48,25.6C52.48,27.7208 50.7608,29.44 48.64,29.44C46.5193,29.44 44.8,27.7208 44.8,25.6C44.8,15.7031 36.777,7.68 26.88,7.68C24.7593,7.68 23.04,5.9608 23.04,3.84Z"
android:fillColor="#0DBD8B"
android:fillType="evenOdd"/>
<path
android:pathData="M40.96,60.16C40.96,62.2808 39.2407,64 37.12,64C22.9815,64 11.52,52.5385 11.52,38.4C11.52,36.2792 13.2392,34.56 15.36,34.56C17.4807,34.56 19.2,36.2792 19.2,38.4C19.2,48.2969 27.223,56.32 37.12,56.32C39.2407,56.32 40.96,58.0392 40.96,60.16Z"
android:fillColor="#0DBD8B"
android:fillType="evenOdd"/>
<path
android:pathData="M3.84,40.96C1.7192,40.96 -0,39.2407 -0,37.12C-0,22.9815 11.4615,11.52 25.6,11.52C27.7208,11.52 29.44,13.2392 29.44,15.36C29.44,17.4807 27.7208,19.2 25.6,19.2C15.7031,19.2 7.68,27.223 7.68,37.12C7.68,39.2407 5.9608,40.96 3.84,40.96Z"
android:fillColor="#0DBD8B"
android:fillType="evenOdd"/>
<path
android:pathData="M60.16,23.04C62.2808,23.04 64,24.7593 64,26.88C64,41.0185 52.5385,52.48 38.4,52.48C36.2792,52.48 34.56,50.7608 34.56,48.64C34.56,46.5193 36.2792,44.8 38.4,44.8C48.2969,44.8 56.32,36.777 56.32,26.88C56.32,24.7593 58.0392,23.04 60.16,23.04Z"
android:fillColor="#0DBD8B"
android:fillType="evenOdd"/>
</vector>

View file

@ -0,0 +1,17 @@
package io.element.android.x.features.login
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}

1
features/messages/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,41 @@
plugins {
id 'com.android.library'
id 'org.jetbrains.kotlin.android'
}
android {
namespace 'io.element.android.x.features.messages'
compileSdk 32
defaultConfig {
minSdk 24
targetSdk 32
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
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
}
kotlinOptions {
jvmTarget = '1.8'
}
}
dependencies {
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.appcompat:appcompat:1.4.1'
implementation 'com.google.android.material:material:1.5.0'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}

View file

21
features/messages/proguard-rules.pro vendored Normal file
View file

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View file

@ -0,0 +1,24 @@
package io.element.android.x.features.messages
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("io.element.android.x.features.messages.test", appContext.packageName)
}
}

View file

@ -1,8 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity android:name="LoginActivity" />
</application>
</manifest> </manifest>

View file

@ -0,0 +1,17 @@
package io.element.android.x.features.messages
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}

1
features/roomlist/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,18 @@
plugins {
id("io.element.android-compose")
}
android {
namespace = "io.element.android.x.features.roomlist"
}
dependencies {
implementation(project(":libraries:core"))
implementation(project(":libraries:matrix"))
implementation(project(":libraries:designsystem"))
implementation("com.airbnb.android:mavericks-compose:3.0.1")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.3")
androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0")
}

View file

21
features/roomlist/proguard-rules.pro vendored Normal file
View file

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View file

@ -0,0 +1,24 @@
package io.element.android.x.features.roomlist
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("io.element.android.x.features.roomlist.test", appContext.packageName)
}
}

View file

@ -1,8 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity android:name="RoomListActivity" />
</application>
</manifest> </manifest>

View file

@ -1,4 +1,4 @@
package io.element.android.x.ui.screen.roomlist package io.element.android.x.features.roomlist
data class MatrixUser( data class MatrixUser(
val username: String? = null, val username: String? = null,

View file

@ -1,7 +1,6 @@
package io.element.android.x.ui.screen.roomlist package io.element.android.x.features.roomlist
sealed interface RoomListActions { sealed interface RoomListActions {
object Init : RoomListActions
object LoadMore : RoomListActions object LoadMore : RoomListActions
object Logout : RoomListActions object Logout : RoomListActions
} }

View file

@ -0,0 +1,70 @@
package io.element.android.x.features.roomlist
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
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.Success
import com.airbnb.mvrx.compose.collectAsState
import com.airbnb.mvrx.compose.mavericksViewModel
import org.matrix.rustcomponents.sdk.Room
@Composable
fun RoomListScreen(
viewModel: RoomListViewModel = mavericksViewModel(),
onRoomClicked: (String) -> Unit = { }
) {
val state by viewModel.collectAsState()
RoomListContent(state, onRoomClicked)
}
@Composable
fun RoomListContent(
state: RoomListViewState,
onRoomClicked: (String) -> Unit
) {
Surface(color = MaterialTheme.colorScheme.background) {
Column(
modifier = Modifier.fillMaxSize()
) {
val rooms = state.rooms
if (rooms is Success) {
LazyColumn {
items(rooms()) { room ->
RoomItem(room = room) {
onRoomClicked(it)
}
}
}
}
}
}
}
@Composable
private fun RoomItem(
modifier: Modifier = Modifier,
room: Room,
onClick: (String) -> Unit
) {
Row(verticalAlignment = Alignment.CenterVertically,
modifier = modifier
.clickable {
onClick(room.id())
}
.fillMaxWidth()
) {
Column(modifier = modifier.padding(8.dp)) {
Text(text = "Room: ${room.name() ?: room.id()}")
Text(text = if (room.isDirect()) "Direct" else "Room")
}
}
}

View file

@ -1,12 +1,12 @@
package io.element.android.x.ui.screen.roomlist package io.element.android.x.features.roomlist
import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.MavericksViewModel import com.airbnb.mvrx.MavericksViewModel
import com.airbnb.mvrx.Success import com.airbnb.mvrx.Success
import io.element.android.x.core.data.tryOrNull import io.element.android.x.core.data.tryOrNull
import io.element.android.x.sdk.matrix.MatrixClient import io.element.android.x.matrix.MatrixClient
import io.element.android.x.sdk.matrix.MatrixInstance import io.element.android.x.matrix.MatrixInstance
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.matrix.rustcomponents.sdk.Room import org.matrix.rustcomponents.sdk.Room
import org.matrix.rustcomponents.sdk.StoppableSpawn import org.matrix.rustcomponents.sdk.StoppableSpawn
@ -19,9 +19,12 @@ class RoomListViewModel(initialState: RoomListViewState) :
private var sync: StoppableSpawn? = null private var sync: StoppableSpawn? = null
private val matrix = MatrixInstance.getInstance() private val matrix = MatrixInstance.getInstance()
init {
handleInit()
}
fun handle(action: RoomListActions) { fun handle(action: RoomListActions) {
when (action) { when (action) {
RoomListActions.Init -> handleInit()
RoomListActions.LoadMore -> TODO() RoomListActions.LoadMore -> TODO()
RoomListActions.Logout -> handleLogout() RoomListActions.Logout -> handleLogout()
} }

View file

@ -1,4 +1,4 @@
package io.element.android.x.ui.screen.roomlist package io.element.android.x.features.roomlist
import com.airbnb.mvrx.Async import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.MavericksState

View file

@ -0,0 +1,17 @@
package io.element.android.x.features.roomlist
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}

67
gradle/libs.versions.toml Normal file
View file

@ -0,0 +1,67 @@
[versions]
# Project
android_gradle_plugin = "7.3.1"
kotlin = "1.7.20"
# AndroidX
material = "1.6.1"
corektx = "1.9.0"
datastore = "1.0.0"
# Compose
compose_compiler = "1.3.2"
compose_bom = "2022.10.00"
# Coroutines
coroutines = "1.6.4"
# Accompanist
accompanist = "0.27.0"
# Test
test_junit = "4.13.2"
test_runner = "1.4.0"
test_core = "1.4.0"
test_mockk = "1.13.2"
test_uiautomator = "2.2.0"
test_junitext = "1.1.3"
test_barista = "4.2.0"
test_hamcrest = "2.2"
test_orchestrator = "1.4.1"
[libraries]
# Project
android_gradle_plugin = { module = "com.android.tools.build:gradle", version.ref = "android_gradle_plugin" }
kotlin_gradle_plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
# AndroidX
androidx_material = { module = "com.google.android.material:material", version.ref = "material" }
androidx_corektx = { module = "androidx.core:core-ktx", version.ref = "corektx" }
androidx_datastore = { module = "androidx.datastore:datastore-preferences", version.ref = "datastore" }
androidx_compose_bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose_bom" }
androidx_compose_foundation = { group = "androidx.compose.foundation", name = "foundation" }
# Coroutines
coroutines_core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
coroutines_test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" }
# Accompanist
accompanist_animation = { module = "com.google.accompanist:accompanist-navigation-animation", version.ref = "accompanist" }
accompanist_permission = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist" }
accompanist_material = { module = "com.google.accompanist:accompanist-navigation-material", version.ref = "accompanist" }
# Test
test_junit = { module = "junit:junit", version.ref = "test_junit" }
test_runner = { module = "androidx.test:runner", version.ref = "test_runner" }
test_core = { module = "androidx.test:core", version.ref = "test_core" }
test_corektx = { module = "androidx.test:core-ktx", version.ref = "test_core" }
test_uiautomator = { module = "androidx.test.uiautomator:uiautomator", version.ref = "test_uiautomator" }
test_junitext = { module = "androidx.test.ext:junit", version.ref = "test_junitext" }
test_mockk = { module = "io.mockk:mockk", version.ref = "test_mockk" }
test_barista = { module = "com.adevinta.android:barista", version.ref = "test_barista" }
test_hamcrest = { module = "org.hamcrest:hamcrest", version.ref = "test_hamcrest" }
test_orchestrator = { module = "androidx.test:orchestrator", version.ref = "test_orchestrator" }
[bundles]

View file

@ -28,7 +28,7 @@ android {
compose true compose true
} }
composeOptions { composeOptions {
kotlinCompilerExtensionVersion compose_version kotlinCompilerExtensionVersion "1.3.2"
} }
kotlinOptions { kotlinOptions {
jvmTarget = '1.8' jvmTarget = '1.8'
@ -39,13 +39,13 @@ dependencies {
implementation 'androidx.core:core-ktx:1.9.0' implementation 'androidx.core:core-ktx:1.9.0'
implementation 'androidx.appcompat:appcompat:1.5.1' implementation 'androidx.appcompat:appcompat:1.5.1'
implementation 'com.google.android.material:material:1.6.1' implementation 'com.google.android.material:material:1.7.0'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1' implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1'
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1" implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1"
implementation 'androidx.activity:activity-compose:1.6.0' implementation 'androidx.activity:activity-compose:1.6.1'
implementation 'androidx.fragment:fragment-ktx:1.5.3' implementation 'androidx.fragment:fragment-ktx:1.5.3'
implementation 'com.airbnb.android:mavericks-compose:2.7.0' implementation 'com.airbnb.android:mavericks-compose:3.0.1'
} }

1
libraries/designsystem/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,12 @@
plugins {
id("io.element.android-compose")
}
android {
namespace = "io.element.android.x.libraries.designsystem"
dependencies {
// Should not be there, but this is a POC
implementation("io.coil-kt:coil-compose:2.2.1")
}
}

View file

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.kts.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>

View file

@ -1,4 +1,4 @@
package io.element.android.x.ui.theme package io.element.android.x.designsystem
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color

View file

@ -1,4 +1,4 @@
package io.element.android.x.ui.theme package io.element.android.x.designsystem
import android.app.Activity import android.app.Activity
import android.os.Build import android.os.Build

View file

@ -1,4 +1,4 @@
package io.element.android.x.ui.theme package io.element.android.x.designsystem
import androidx.compose.material3.Typography import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle

View file

@ -1,4 +1,4 @@
package io.element.android.x.ui.theme.components package io.element.android.x.designsystem.components
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.Text import androidx.compose.material3.Text

View file

@ -1,4 +1,4 @@
package io.element.android.x.ui.theme.components package io.element.android.x.designsystem.components
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api

View file

@ -0,0 +1,3 @@
package io.element.android.x.matrix
internal const val LOG_TAG = "Matrix"

View file

@ -1,8 +1,8 @@
package io.element.android.x.sdk.matrix package io.element.android.x.matrix
import android.content.Context import android.content.Context
import io.element.android.x.sdk.matrix.store.SessionStore import io.element.android.x.matrix.store.SessionStore
import io.element.android.x.sdk.matrix.util.logError import io.element.android.x.matrix.util.logError
import org.matrix.rustcomponents.sdk.AuthenticationService import org.matrix.rustcomponents.sdk.AuthenticationService
import org.matrix.rustcomponents.sdk.ClientBuilder import org.matrix.rustcomponents.sdk.ClientBuilder
import java.io.File import java.io.File

View file

@ -1,7 +1,7 @@
package io.element.android.x.sdk.matrix package io.element.android.x.matrix
import android.util.Log import android.util.Log
import io.element.android.x.sdk.matrix.store.SessionStore import io.element.android.x.matrix.store.SessionStore
import org.matrix.rustcomponents.sdk.* import org.matrix.rustcomponents.sdk.*
class MatrixClient internal constructor( class MatrixClient internal constructor(

View file

@ -1,4 +1,4 @@
package io.element.android.x.sdk.matrix package io.element.android.x.matrix
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context

View file

@ -1,4 +1,4 @@
package io.element.android.x.sdk.matrix package io.element.android.x.matrix
import android.util.Log import android.util.Log
import org.matrix.rustcomponents.sdk.Client import org.matrix.rustcomponents.sdk.Client

View file

@ -1,4 +1,4 @@
package io.element.android.x.sdk.matrix.store package io.element.android.x.matrix.store
import android.content.Context import android.content.Context
import androidx.datastore.core.DataStore import androidx.datastore.core.DataStore
@ -10,6 +10,10 @@ import kotlinx.coroutines.flow.firstOrNull
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "elementx_sessions") private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "elementx_sessions")
private val userIdPreference = stringPreferencesKey("userId")
// TODO It contains the access token, so it has to be stored in a more secured storage.
// I would expect the Rust SDK to provide a more obscure token.
private val restoreTokenPreference = stringPreferencesKey("restoreToken")
internal class SessionStore( internal class SessionStore(
context: Context context: Context
@ -21,10 +25,6 @@ internal class SessionStore(
private val store = context.dataStore private val store = context.dataStore
private val userIdPreference = stringPreferencesKey("userId")
// TODO It contains the access token, so it has to be stored in a more secured storage.
// I would expect the Rust SDK to provide a more obscure token.
private val restoreTokenPreference = stringPreferencesKey("restoreToken")
suspend fun storeData(sessionData: SessionData) { suspend fun storeData(sessionData: SessionData) {
store.edit { prefs -> store.edit { prefs ->

View file

@ -1,7 +1,7 @@
package io.element.android.x.sdk.matrix.util package io.element.android.x.matrix.util
import android.util.Log import android.util.Log
import io.element.android.x.sdk.matrix.LOG_TAG import io.element.android.x.matrix.LOG_TAG
import org.matrix.rustcomponents.sdk.ClientException import org.matrix.rustcomponents.sdk.ClientException
fun logError(throwable: Throwable) { fun logError(throwable: Throwable) {

View file

@ -1,3 +0,0 @@
package io.element.android.x.sdk.matrix
internal const val LOG_TAG = "Matrix"

View file

@ -1,61 +0,0 @@
plugins {
id 'com.android.library'
id 'org.jetbrains.kotlin.android'
}
android {
namespace 'io.element.android.x.ui.screen.login'
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'
}

View file

@ -1,129 +0,0 @@
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.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.*
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.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
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
class LoginActivity : ComponentActivity() {
@OptIn(ExperimentalMaterial3Api::class)
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
) {
Box(modifier = Modifier.fillMaxSize()) {
Column(
modifier = Modifier.fillMaxSize(),
) {
val viewModel: LoginViewModel = mavericksViewModel()
val state by viewModel.collectAsState()
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 = {
viewModel.handle(LoginActions.SetHomeserver(it))
},
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Uri,
),
)
OutlinedTextField(
value = state.login,
modifier = Modifier
.fillMaxWidth()
.padding(top = 8.dp),
onValueChange = {
viewModel.handle(LoginActions.SetLogin(it))
},
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Text,
),
)
OutlinedTextField(
value = state.password,
modifier = Modifier
.fillMaxWidth()
.padding(top = 8.dp),
onValueChange = {
viewModel.handle(LoginActions.SetPassword(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)
)
}
VectorButton(
text = "Submit",
onClick = {
viewModel.handle(LoginActions.Submit)
},
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)
)
}
if (state.isLoggedIn is Success) {
openRoomList()
}
}
}
}
}
}
}
private fun openRoomList() {
setResult(Activity.RESULT_OK)
finish()
}
}

View file

@ -1,22 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="64dp"
android:height="64dp"
android:viewportWidth="64"
android:viewportHeight="64">
<path
android:pathData="M23.04,3.84C23.04,1.7192 24.7593,0 26.88,0C41.0185,0 52.48,11.4615 52.48,25.6C52.48,27.7208 50.7608,29.44 48.64,29.44C46.5193,29.44 44.8,27.7208 44.8,25.6C44.8,15.7031 36.777,7.68 26.88,7.68C24.7593,7.68 23.04,5.9608 23.04,3.84Z"
android:fillColor="#0DBD8B"
android:fillType="evenOdd"/>
<path
android:pathData="M40.96,60.16C40.96,62.2808 39.2407,64 37.12,64C22.9815,64 11.52,52.5385 11.52,38.4C11.52,36.2792 13.2392,34.56 15.36,34.56C17.4807,34.56 19.2,36.2792 19.2,38.4C19.2,48.2969 27.223,56.32 37.12,56.32C39.2407,56.32 40.96,58.0392 40.96,60.16Z"
android:fillColor="#0DBD8B"
android:fillType="evenOdd"/>
<path
android:pathData="M3.84,40.96C1.7192,40.96 -0,39.2407 -0,37.12C-0,22.9815 11.4615,11.52 25.6,11.52C27.7208,11.52 29.44,13.2392 29.44,15.36C29.44,17.4807 27.7208,19.2 25.6,19.2C15.7031,19.2 7.68,27.223 7.68,37.12C7.68,39.2407 5.9608,40.96 3.84,40.96Z"
android:fillColor="#0DBD8B"
android:fillType="evenOdd"/>
<path
android:pathData="M60.16,23.04C62.2808,23.04 64,24.7593 64,26.88C64,41.0185 52.5385,52.48 38.4,52.48C36.2792,52.48 34.56,50.7608 34.56,48.64C34.56,46.5193 36.2792,44.8 38.4,44.8C48.2969,44.8 56.32,36.777 56.32,26.88C56.32,24.7593 58.0392,23.04 60.16,23.04Z"
android:fillColor="#0DBD8B"
android:fillType="evenOdd"/>
</vector>

View file

@ -1,62 +0,0 @@
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'
implementation 'io.coil-kt:coil-compose:2.2.1'
}

View file

@ -1,126 +0,0 @@
package io.element.android.x.ui.screen.roomlist
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.Image
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ExitToApp
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
import coil.compose.rememberAsyncImagePainter
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.Avatar
import org.matrix.rustcomponents.sdk.Room
class RoomListActivity : ComponentActivity() {
private var initDone = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ElementXTheme {
val viewModel: RoomListViewModel = mavericksViewModel()
if (!initDone) {
initDone = true
viewModel.handle(RoomListActions.Init)
}
val state = viewModel.collectAsState()
// 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()
) {
OptionMenu(state.value.user, viewModel)
val rooms = state.value.rooms
if (rooms is Success) {
LazyColumn {
items(rooms()) { room ->
RoomCompose(room) {
Toast.makeText(
this@RoomListActivity,
"Room $it clicked!",
Toast.LENGTH_SHORT
).show()
}
}
}
}
}
}
if (state.value.logoutAction is Success) {
finish()
}
}
}
}
@Composable
private fun RoomCompose(room: Room, onClick: (String) -> Unit) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { onClick.invoke(room.id()) },
) {
Image(
painter = rememberAsyncImagePainter(model = room.avatarUrl()),
contentDescription = null,
modifier = Modifier.size(32.dp),
)
Spacer(modifier = Modifier.width(8.dp))
Column(
modifier = Modifier
.fillMaxWidth()
.height(32.dp),
) {
Text(text = "Room: ${room.name() ?: room.id()}")
Text(text = if (room.isDirect()) "Direct" else "Room")
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun OptionMenu(matrixUser: MatrixUser, viewModel: RoomListViewModel) {
TopAppBar(
title = {
Row(
modifier = Modifier.fillMaxWidth()
) {
Avatar(data = matrixUser.avatarData)
Spacer(modifier = Modifier.width(8.dp))
Text("${matrixUser.username}")
}
},
actions = {
IconButton(
onClick = { viewModel.handle(RoomListActions.Logout) }
) {
Icon(Icons.Default.ExitToApp, contentDescription = "logout")
}
}
)
}
}

View file

@ -1,52 +0,0 @@
plugins {
id 'com.android.library'
id 'org.jetbrains.kotlin.android'
}
android {
namespace 'io.element.android.x.theme'
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.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-viewmodel-compose:2.5.1"
implementation 'io.coil-kt:coil-compose:2.2.1'
}

14
plugins/build.gradle.kts Normal file
View file

@ -0,0 +1,14 @@
plugins {
`kotlin-dsl`
`kotlin-dsl-precompiled-script-plugins`
}
repositories {
mavenCentral()
google()
}
dependencies {
implementation(libs.android.gradle.plugin)
implementation(libs.kotlin.gradle.plugin)
}

View file

@ -0,0 +1,10 @@
dependencyResolutionManagement {
repositories {
mavenCentral()
}
versionCatalogs {
create("libs") {
from(files("../gradle/libs.versions.toml"))
}
}
}

View file

@ -0,0 +1,17 @@
import org.gradle.api.JavaVersion
import org.gradle.api.artifacts.VersionCatalog
import org.gradle.jvm.toolchain.JavaLanguageVersion
val VersionCatalog.composeVersion: String
get() = findVersion("compose_compiler").get().requiredVersion
object Versions {
const val versionCode = 100100
const val versionName = "0.1.0"
const val compileSdk = 33
const val targetSdk = 33
const val minSdk = 24
val javaCompileVersion = JavaVersion.VERSION_11
val javaLanguageVersion: JavaLanguageVersion = JavaLanguageVersion.of(11)
}

View file

@ -0,0 +1,50 @@
package extension
import Versions
import com.android.build.api.dsl.CommonExtension
import com.android.build.api.dsl.LibraryExtension
import composeVersion
import org.gradle.api.artifacts.VersionCatalog
fun CommonExtension<*, *, *, *>.androidConfig() {
defaultConfig {
compileSdk = Versions.compileSdk
minSdk = Versions.minSdk
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
testOptions {
unitTests.isReturnDefaultValues = true
}
}
fun CommonExtension<*, *, *, *>.composeConfig() {
buildFeatures {
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.3.2"
}
packagingOptions {
resources.excludes.apply {
add("META-INF/AL2.0")
add("META-INF/LGPL2.1")
}
}
}
fun LibraryExtension.proguardConfig() {
buildTypes {
getByName("release") {
isMinifyEnabled = true
proguardFiles("proguard-android.txt", "proguard-rules.pro")
consumerProguardFiles("proguard-rules.pro")
}
}
}

View file

@ -0,0 +1,29 @@
import extension.androidConfig
import extension.composeConfig
import extension.proguardConfig
plugins {
id("com.android.library")
id("kotlin-android")
}
android {
androidConfig()
proguardConfig()
composeConfig()
}
dependencies {
implementation(platform("androidx.compose:compose-bom:2022.10.00"))
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.material3:material3")
implementation("androidx.compose.ui:ui-tooling-preview")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.5.1")
implementation("androidx.activity:activity-compose:1.6.1")
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1")
implementation("com.airbnb.android:mavericks-compose:3.0.1")
debugImplementation("androidx.compose.ui:ui-tooling")
debugImplementation("androidx.compose.ui:ui-test-manifest")
}

View file

@ -0,0 +1,12 @@
import extension.androidConfig
import extension.proguardConfig
plugins {
id("com.android.library")
id("kotlin-android")
}
android {
androidConfig()
proguardConfig()
}

View file

@ -1,5 +1,6 @@
pluginManagement { pluginManagement {
repositories { repositories {
includeBuild("plugins")
gradlePluginPortal() gradlePluginPortal()
google() google()
mavenCentral() mavenCentral()
@ -11,14 +12,15 @@ dependencyResolutionManagement {
google() google()
mavenCentral() mavenCentral()
flatDir { flatDir {
dirs 'libraries/sdk/matrix/libs' dirs("libraries/matrix/libs")
} }
} }
} }
rootProject.name = "ElementX" rootProject.name = "ElementX"
include ':app' include(":app")
include ':libraries:core' include(":libraries:core")
include ':libraries:ui:theme' include(":libraries:matrix")
include ':libraries:ui:screens:login' include(":features:login")
include ':libraries:ui:screens:roomlist' include(":features:roomlist")
include ':libraries:sdk:matrix' include(":features:messages")
include(":libraries:designsystem")