diff --git a/app/src/androidTest/java/eu/steffo/twom/ExampleInstrumentedTest.kt b/app/src/androidTest/java/eu/steffo/twom/ExampleInstrumentedTest.kt deleted file mode 100644 index c19be2f..0000000 --- a/app/src/androidTest/java/eu/steffo/twom/ExampleInstrumentedTest.kt +++ /dev/null @@ -1,24 +0,0 @@ -package eu.steffo.twom - -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("eu.steffo.twom", appContext.packageName) - } -} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 847bd61..b85341f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -31,7 +31,7 @@ @@ -42,15 +42,15 @@ diff --git a/app/src/main/java/eu/steffo/twom/activities/CreateRoomActivity.kt b/app/src/main/java/eu/steffo/twom/activities/CreateRoomActivity.kt new file mode 100644 index 0000000..89d1eaa --- /dev/null +++ b/app/src/main/java/eu/steffo/twom/activities/CreateRoomActivity.kt @@ -0,0 +1,49 @@ +package eu.steffo.twom.activities + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.result.contract.ActivityResultContract +import eu.steffo.twom.composables.createroom.CreateRoomScaffold + + +data class CreateRoomActivityResult( + val name: String, + val description: String, + val avatarUri: String?, +) + + +class CreateRoomActivity : ComponentActivity() { + companion object { + const val NAME_EXTRA = "name" + const val DESCRIPTION_EXTRA = "description" + const val AVATAR_EXTRA = "avatar" + } + + class Contract : ActivityResultContract() { + override fun createIntent(context: Context, input: Unit): Intent { + return Intent(context, CreateRoomActivity::class.java) + } + + override fun parseResult(resultCode: Int, intent: Intent?): CreateRoomActivityResult? { + return when (resultCode) { + RESULT_OK -> CreateRoomActivityResult( + name = intent!!.getStringExtra(NAME_EXTRA)!!, + description = intent.getStringExtra(DESCRIPTION_EXTRA)!!, + avatarUri = intent.getStringExtra(AVATAR_EXTRA), + ) + + else -> null + } + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContent { CreateRoomScaffold() } + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/steffo/twom/activities/LoginActivity.kt b/app/src/main/java/eu/steffo/twom/activities/LoginActivity.kt new file mode 100644 index 0000000..73926c0 --- /dev/null +++ b/app/src/main/java/eu/steffo/twom/activities/LoginActivity.kt @@ -0,0 +1,25 @@ +package eu.steffo.twom.activities + +import android.content.Context +import android.content.Intent +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.result.contract.ActivityResultContract +import eu.steffo.twom.composables.login.LoginScaffold + + +class LoginActivity : ComponentActivity() { + class Contract : ActivityResultContract() { + override fun createIntent(context: Context, input: Unit): Intent { + return Intent(context, LoginActivity::class.java) + } + + override fun parseResult(resultCode: Int, intent: Intent?) {} + } + + override fun onStart() { + super.onStart() + + setContent { LoginScaffold() } + } +} diff --git a/app/src/main/java/eu/steffo/twom/main/MainActivity.kt b/app/src/main/java/eu/steffo/twom/activities/MainActivity.kt similarity index 82% rename from app/src/main/java/eu/steffo/twom/main/MainActivity.kt rename to app/src/main/java/eu/steffo/twom/activities/MainActivity.kt index b713322..5357450 100644 --- a/app/src/main/java/eu/steffo/twom/main/MainActivity.kt +++ b/app/src/main/java/eu/steffo/twom/activities/MainActivity.kt @@ -1,4 +1,4 @@ -package eu.steffo.twom.main +package eu.steffo.twom.activities import android.content.Intent import android.net.Uri @@ -11,10 +11,8 @@ import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts import androidx.core.net.toFile import androidx.lifecycle.lifecycleScope -import eu.steffo.twom.create.CreateActivity -import eu.steffo.twom.login.LoginActivity +import eu.steffo.twom.composables.main.MainScaffold import eu.steffo.twom.matrix.TwoMMatrix -import eu.steffo.twom.room.RoomActivity import kotlinx.coroutines.launch import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams @@ -77,6 +75,12 @@ class MainActivity : ComponentActivity() { currentSession.open() currentSession.syncService().startSync(true) currentSession.addListener(OpenSessionListener(this::resetContent)) + + Log.d( + "Main", + "Opened session, recomposing..." + ) + resetContent() } } @@ -95,42 +99,8 @@ class MainActivity : ComponentActivity() { } } - private fun onClickLogin() { - Log.d("Main", "Clicked login, launching login activity...") - val intent = Intent(applicationContext, LoginActivity::class.java) - loginLauncher.launch(intent) - } + private fun destroySession() { - private fun onLogin(result: ActivityResult) { - Log.d("Main", "Received result from login activity: $result") - when (result.resultCode) { - RESULT_OK -> { - Log.d( - "Main", - "Login activity returned a successful result, trying to get session..." - ) - fetchLastSession() - openSession() - } - else -> { - Log.d("Main", "Login activity was cancelled.") - } - } - } - - private fun onClickLogout() { - lifecycleScope.launch { - val signedOutSession = session!! - - Log.d("Main", "Clicked logout, recomposing...") - session = null - resetContent() - - Log.d("Main", "Done recomposing, now signing out...") - signedOutSession.signOutService().signOut(true) - - Log.d("Main", "Done logging out!") - } } private fun onClickRoom(roomId: String) { @@ -146,7 +116,7 @@ class MainActivity : ComponentActivity() { private fun onClickCreate() { Log.d("Main", "Clicked the New button, launching create activity...") - val intent = Intent(applicationContext, CreateActivity::class.java) + val intent = Intent(applicationContext, CreateRoomActivity::class.java) createLauncher.launch(intent) } @@ -154,10 +124,10 @@ class MainActivity : ComponentActivity() { Log.d("Main", "Received result from create activity: $result") if (result.resultCode == RESULT_OK) { val intent: Intent = result.data!! - val name = intent.getStringExtra(CreateActivity.NAME_EXTRA) - val description = intent.getStringExtra(CreateActivity.DESCRIPTION_EXTRA) + val name = intent.getStringExtra(CreateRoomActivity.NAME_EXTRA) + val description = intent.getStringExtra(CreateRoomActivity.DESCRIPTION_EXTRA) @Suppress("DEPRECATION") val avatarUri = - intent.getParcelableExtra(CreateActivity.AVATAR_EXTRA) + intent.getParcelableExtra(CreateRoomActivity.AVATAR_EXTRA) if (name == null) { Log.w("Main", "Result from create activity did not have `name` extra set") @@ -265,12 +235,32 @@ class MainActivity : ComponentActivity() { private fun resetContent() { Log.d("Main", "Recomposing...") setContent { - MatrixActivityScaffold( - onClickLogin = this::onClickLogin, - onClickLogout = this::onClickLogout, - onClickRoom = this::onClickRoom, - onClickCreate = this::onClickCreate, + // TODO: Check this with a clearer mind + MainScaffold( session = session, + processLogin = { + Log.d( + "Main", + "Login activity returned a successful result, trying to get session..." + ) + fetchLastSession() + openSession() + + }, + processLogout = { + val signedOutSession = session!! + + Log.d("Main", "Clicked logout, recomposing...") + session = null + resetContent() + + Log.d("Main", "Done recomposing, now signing out...") + lifecycleScope.launch { + signedOutSession.signOutService().signOut(true) + } + + Log.d("Main", "Done logging out!") + }, ) } } diff --git a/app/src/main/java/eu/steffo/twom/room/RoomActivity.kt b/app/src/main/java/eu/steffo/twom/activities/RoomActivity.kt similarity index 70% rename from app/src/main/java/eu/steffo/twom/room/RoomActivity.kt rename to app/src/main/java/eu/steffo/twom/activities/RoomActivity.kt index cbedcd3..6ecfdb8 100644 --- a/app/src/main/java/eu/steffo/twom/room/RoomActivity.kt +++ b/app/src/main/java/eu/steffo/twom/activities/RoomActivity.kt @@ -1,10 +1,14 @@ -package eu.steffo.twom.room +package eu.steffo.twom.activities +import android.content.Context +import android.content.Intent import android.os.Bundle import android.util.Log import androidx.activity.ComponentActivity import androidx.activity.compose.setContent +import androidx.activity.result.contract.ActivityResultContract import eu.steffo.twom.matrix.TwoMMatrix +import eu.steffo.twom.room.RoomActivityScaffold import org.matrix.android.sdk.api.session.Session class RoomActivity : ComponentActivity() { @@ -12,6 +16,16 @@ class RoomActivity : ComponentActivity() { const val ROOM_ID_EXTRA = "roomId" } + class Contract : ActivityResultContract() { + override fun createIntent(context: Context, input: String): Intent { + val intent = Intent(context, RoomActivity::class.java) + intent.putExtra(ROOM_ID_EXTRA, input) + return intent + } + + override fun parseResult(resultCode: Int, intent: Intent?) {} + } + private lateinit var session: Session private fun fetchLastSession() { diff --git a/app/src/main/java/eu/steffo/twom/create/AvatarSelector.kt b/app/src/main/java/eu/steffo/twom/composables/avatar/AvatarPicker.kt similarity index 73% rename from app/src/main/java/eu/steffo/twom/create/AvatarSelector.kt rename to app/src/main/java/eu/steffo/twom/composables/avatar/AvatarPicker.kt index 2bfb5f4..8f6d829 100644 --- a/app/src/main/java/eu/steffo/twom/create/AvatarSelector.kt +++ b/app/src/main/java/eu/steffo/twom/composables/avatar/AvatarPicker.kt @@ -1,4 +1,4 @@ -package eu.steffo.twom.create +package eu.steffo.twom.composables.avatar import android.graphics.Bitmap import androidx.activity.compose.rememberLauncherForActivityResult @@ -16,12 +16,14 @@ import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.Preview import eu.steffo.twom.matrix.avatar.AvatarFromImageBitmap +import eu.steffo.twom.utils.BitmapUtilities @Composable @Preview(widthDp = 40, heightDp = 40) -fun AvatarSelector( +fun AvatarPicker( modifier: Modifier = Modifier, - onSelectAvatar: (bitmap: Bitmap) -> Unit = {}, + fallbackText: String = "?", + onPick: (bitmap: Bitmap) -> Unit = {}, ) { val context = LocalContext.current val resolver = context.contentResolver @@ -32,13 +34,13 @@ fun AvatarSelector( rememberLauncherForActivityResult(ActivityResultContracts.PickVisualMedia()) ImageSelect@{ it ?: return@ImageSelect - val rawBitmap = ImageHandler.getRawBitmap(resolver, it) ?: return@ImageSelect - val orientation = ImageHandler.getOrientation(resolver, it) ?: return@ImageSelect + val rawBitmap = BitmapUtilities.getRawBitmap(resolver, it) ?: return@ImageSelect + val orientation = BitmapUtilities.getOrientation(resolver, it) ?: return@ImageSelect - val correctedBitmap = ImageHandler.squareAndOrient(rawBitmap, orientation) + val correctedBitmap = BitmapUtilities.squareAndOrient(rawBitmap, orientation) selection = correctedBitmap - onSelectAvatar(correctedBitmap) + onPick(correctedBitmap) } Box( @@ -49,6 +51,7 @@ fun AvatarSelector( ) { AvatarFromImageBitmap( bitmap = selection?.asImageBitmap(), + fallbackText = fallbackText, ) } } \ No newline at end of file diff --git a/app/src/main/java/eu/steffo/twom/create/CreateActivityContent.kt b/app/src/main/java/eu/steffo/twom/composables/createroom/CreateRoomForm.kt similarity index 85% rename from app/src/main/java/eu/steffo/twom/create/CreateActivityContent.kt rename to app/src/main/java/eu/steffo/twom/composables/createroom/CreateRoomForm.kt index f4992cd..a40bb0a 100644 --- a/app/src/main/java/eu/steffo/twom/create/CreateActivityContent.kt +++ b/app/src/main/java/eu/steffo/twom/composables/createroom/CreateRoomForm.kt @@ -1,4 +1,4 @@ -package eu.steffo.twom.create +package eu.steffo.twom.composables.createroom import android.net.Uri import androidx.compose.foundation.layout.Column @@ -24,33 +24,32 @@ import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import eu.steffo.twom.R +import eu.steffo.twom.composables.avatar.AvatarPicker import eu.steffo.twom.theme.TwoMPadding +import eu.steffo.twom.utils.BitmapUtilities @Composable -@Preview -fun CreateActivityContent( +@Preview(showBackground = true) +fun CreateRoomForm( modifier: Modifier = Modifier, - onClickCreate: (name: String, description: String, avatarUri: Uri?) -> Unit = { _, _, _ -> }, + onSubmit: (name: String, description: String, avatarUri: Uri?) -> Unit = { _, _, _ -> }, ) { - var name by rememberSaveable { mutableStateOf("") } var description by rememberSaveable { mutableStateOf("") } var avatarUri by rememberSaveable { mutableStateOf(null) } - // val avatarBitmap = if(avatarUri != null) BitmapFactory.decodeFile(avatarUri.toString()).asImageBitmap() else null - Column(modifier) { Row(TwoMPadding.base) { val avatarContentDescription = stringResource(R.string.create_avatar_label) - AvatarSelector( + AvatarPicker( modifier = Modifier .size(60.dp) .clip(MaterialTheme.shapes.medium) .semantics { this.contentDescription = avatarContentDescription }, - onSelectAvatar = SelectAvatar@{ - val cache = ImageHandler.bitmapToCache("createAvatar", it) + onPick = { + val cache = BitmapUtilities.bitmapToCache("createAvatar", it) avatarUri = Uri.fromFile(cache) }, ) @@ -86,7 +85,7 @@ fun CreateActivityContent( modifier = Modifier .fillMaxWidth(), onClick = { - onClickCreate(name, description, avatarUri) + onSubmit(name, description, avatarUri) }, ) { Text(stringResource(R.string.create_complete_text)) diff --git a/app/src/main/java/eu/steffo/twom/composables/createroom/CreateRoomScaffold.kt b/app/src/main/java/eu/steffo/twom/composables/createroom/CreateRoomScaffold.kt new file mode 100644 index 0000000..ce969f8 --- /dev/null +++ b/app/src/main/java/eu/steffo/twom/composables/createroom/CreateRoomScaffold.kt @@ -0,0 +1,49 @@ +package eu.steffo.twom.composables.createroom + +import android.app.Activity +import android.content.Intent +import android.net.Uri +import androidx.activity.ComponentActivity +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview +import eu.steffo.twom.activities.CreateRoomActivity +import eu.steffo.twom.theme.TwoMTheme + +@Composable +@Preview +fun CreateRoomScaffold() { + val context = LocalContext.current + val activity = context as Activity + + fun submitActivity(name: String, description: String, avatarUri: Uri?) { + val resultIntent = Intent() + resultIntent.putExtra(CreateRoomActivity.NAME_EXTRA, name) + resultIntent.putExtra(CreateRoomActivity.DESCRIPTION_EXTRA, description) + // Kotlin cannot use nullable types in Java interop generics + if (avatarUri != null) { + resultIntent.putExtra(CreateRoomActivity.AVATAR_EXTRA, avatarUri) + } + activity.setResult(ComponentActivity.RESULT_OK, resultIntent) + activity.finish() + } + + TwoMTheme { + Scaffold( + topBar = { + CreateActivityTopBar() + }, + content = { + CreateRoomForm( + modifier = Modifier.padding(it), + onSubmit = { name: String, description: String, avatarUri: Uri? -> + submitActivity(name, description, avatarUri) + } + ) + } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/steffo/twom/create/CreateActivityTopBar.kt b/app/src/main/java/eu/steffo/twom/composables/createroom/CreateRoomTopBar.kt similarity index 53% rename from app/src/main/java/eu/steffo/twom/create/CreateActivityTopBar.kt rename to app/src/main/java/eu/steffo/twom/composables/createroom/CreateRoomTopBar.kt index 3c20d12..d078ffd 100644 --- a/app/src/main/java/eu/steffo/twom/create/CreateActivityTopBar.kt +++ b/app/src/main/java/eu/steffo/twom/composables/createroom/CreateRoomTopBar.kt @@ -1,10 +1,6 @@ -package eu.steffo.twom.create +package eu.steffo.twom.composables.createroom -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable @@ -12,24 +8,17 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.Preview import eu.steffo.twom.R +import eu.steffo.twom.composables.navigation.BackIconButton @OptIn(ExperimentalMaterial3Api::class) @Composable @Preview fun CreateActivityTopBar( modifier: Modifier = Modifier, - onClickBack: () -> Unit = {}, ) { TopAppBar( modifier = modifier, - navigationIcon = { - IconButton(onClick = onClickBack) { - Icon( - imageVector = Icons.Filled.ArrowBack, - contentDescription = LocalContext.current.getString(R.string.back) - ) - } - }, + navigationIcon = { BackIconButton() }, title = { Text(LocalContext.current.getString(R.string.create_title)) } ) } \ No newline at end of file diff --git a/app/src/main/java/eu/steffo/twom/composables/errorhandling/ErrorText.kt b/app/src/main/java/eu/steffo/twom/composables/errorhandling/ErrorText.kt new file mode 100644 index 0000000..452134b --- /dev/null +++ b/app/src/main/java/eu/steffo/twom/composables/errorhandling/ErrorText.kt @@ -0,0 +1,19 @@ +package eu.steffo.twom.composables.errorhandling + +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + + +@Composable +fun ErrorText( + text: String, + modifier: Modifier = Modifier, +) { + Text( + modifier = modifier, + text = text, + color = MaterialTheme.colorScheme.error, + ) +} diff --git a/app/src/main/java/eu/steffo/twom/login/PasswordField.kt b/app/src/main/java/eu/steffo/twom/composables/fields/PasswordField.kt similarity index 98% rename from app/src/main/java/eu/steffo/twom/login/PasswordField.kt rename to app/src/main/java/eu/steffo/twom/composables/fields/PasswordField.kt index c6e3c4f..fe61548 100644 --- a/app/src/main/java/eu/steffo/twom/login/PasswordField.kt +++ b/app/src/main/java/eu/steffo/twom/composables/fields/PasswordField.kt @@ -1,4 +1,4 @@ -package eu.steffo.twom.login +package eu.steffo.twom.composables.fields import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.text.KeyboardActions diff --git a/app/src/main/java/eu/steffo/twom/composables/login/LoginForm.kt b/app/src/main/java/eu/steffo/twom/composables/login/LoginForm.kt new file mode 100644 index 0000000..fcb4f6b --- /dev/null +++ b/app/src/main/java/eu/steffo/twom/composables/login/LoginForm.kt @@ -0,0 +1,231 @@ +package eu.steffo.twom.composables.login + +import android.util.Log +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.Button +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ProgressIndicatorDefaults +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview +import eu.steffo.twom.R +import eu.steffo.twom.composables.errorhandling.ErrorText +import eu.steffo.twom.composables.errorhandling.LocalizableError +import eu.steffo.twom.composables.fields.PasswordField +import eu.steffo.twom.matrix.TwoMMatrix +import eu.steffo.twom.theme.TwoMPadding +import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig +import org.matrix.android.sdk.api.auth.data.LoginFlowResult +import org.matrix.android.sdk.api.auth.login.LoginWizard +import org.matrix.android.sdk.api.auth.wellknown.WellknownResult +import org.matrix.android.sdk.api.failure.MatrixIdFailure +import org.matrix.android.sdk.api.session.Session + + +enum class LoginStep(val step: Int) { + NONE(0), + SERVICE(1), + WELLKNOWN(2), + FLOWS(3), + WIZARD(4), + LOGIN(5), + DONE(6), +} + + +// TODO: Localize error messages +@Composable +@Preview(showBackground = true) +fun LoginForm( + modifier: Modifier = Modifier, + onLogin: (session: Session) -> Unit = {}, +) { + val scope = rememberCoroutineScope() + + var username by rememberSaveable { mutableStateOf("") } + var password by rememberSaveable { mutableStateOf("") } + + var loginStep by rememberSaveable { mutableStateOf(LoginStep.NONE) } + val error by remember { mutableStateOf(LocalizableError()) } + + suspend fun doLogin() { + error.clear() + + Log.d("Login", "Getting authentication service...") + loginStep = LoginStep.SERVICE + val auth = TwoMMatrix.matrix.authenticationService() + + Log.d("Login", "Resetting authentication service...") + auth.reset() + + Log.d("Login", "Retrieving .well-known data for: $username") + loginStep = LoginStep.WELLKNOWN + lateinit var wellKnown: WellknownResult + try { + wellKnown = auth.getWellKnownData(username, null) + } catch (e: MatrixIdFailure.InvalidMatrixId) { + Log.d( + "Login", + "User seems to have input an invalid Matrix ID: $username", + e + ) + error.set(R.string.login_error_username_invalid) + return + } catch (e: Throwable) { + Log.e( + "Login", + "Something went wrong while retrieving .well-known data for: $username", + e + ) + error.set(R.string.login_error_wellknown_generic, e) + return + } + if (wellKnown !is WellknownResult.Prompt) { + Log.w( + "Login", + "Data is not .well-known for: $username" + ) + error.set(R.string.login_error_wellknown_missing) + return + } + + Log.d("Login", "Retrieving login flows for: ${wellKnown.homeServerUrl}") + loginStep = LoginStep.FLOWS + @Suppress("ASSIGNED_BUT_NEVER_ACCESSED_VARIABLE") + lateinit var flows: LoginFlowResult + try { + @Suppress("UNUSED_VALUE") + flows = auth.getLoginFlow( + HomeServerConnectionConfig + .Builder() + .withHomeServerUri(wellKnown.homeServerUrl) + .build() + ) + } catch (e: Throwable) { + Log.e( + "Login", + "Something went wrong while retrieving login flows for: ${wellKnown.homeServerUrl}", + e + ) + error.set(R.string.login_error_flows_generic, e) + return + } + + Log.d("Login", "Creating login wizard...") + loginStep = LoginStep.WIZARD + lateinit var wizard: LoginWizard + try { + wizard = auth.getLoginWizard() // Why is this stateful? Aargh. + } catch (e: Throwable) { + // TODO: It sure would be nice to know which exceptions can be thrown here. + Log.e( + "Login", + "Something went wrong while setting up the login wizard.", + e + ) + error.set(R.string.login_error_wizard_generic, e) + return + } + + Log.d("Login", "Logging in as: $username") + loginStep = LoginStep.LOGIN + lateinit var session: Session + try { + session = wizard.login( + login = username, + password = password, + initialDeviceName = "TwoM (Android)", + ) + } catch (e: Throwable) { + Log.e( + "Login", + "Something went wrong while logging in as: $username", + e + ) + error.set(R.string.login_error_login_generic, e) + return + } + + Log.d( + "Login", + "Logged in successfully with session id: ${session.sessionId}" + ) + loginStep = LoginStep.DONE + + onLogin(session) + } + + Column(modifier) { + LinearProgressIndicator( + modifier = Modifier.fillMaxWidth(), + progress = loginStep.step.toFloat() / LoginStep.DONE.step.toFloat(), + color = if (error.occurred()) MaterialTheme.colorScheme.error else ProgressIndicatorDefaults.linearColor + ) + Row(TwoMPadding.base) { + Text(LocalContext.current.getString(R.string.login_text)) + } + Row(TwoMPadding.base) { + TextField( + modifier = Modifier.fillMaxWidth(), + singleLine = true, + value = username, + onValueChange = { username = it }, + label = { + Text(LocalContext.current.getString(R.string.login_username_label)) + }, + placeholder = { + Text(LocalContext.current.getString(R.string.login_username_placeholder)) + }, + supportingText = { + Text(LocalContext.current.getString(R.string.login_username_supporting)) + }, + ) + } + Row(TwoMPadding.base) { + PasswordField( + modifier = Modifier.fillMaxWidth(), + singleLine = true, + value = password, + onValueChange = { password = it }, + label = { + Text(LocalContext.current.getString(R.string.login_password_label)) + }, + placeholder = { + Text(LocalContext.current.getString(R.string.login_password_placeholder)) + }, + supportingText = { + Text(LocalContext.current.getString(R.string.login_password_supporting)) + }, + ) + } + Row(TwoMPadding.base) { + Button( + modifier = Modifier.fillMaxWidth(), + enabled = (username != "" && (loginStep == LoginStep.NONE || error.occurred())), + onClick = { + scope.launch { doLogin() } + }, + ) { + Text(LocalContext.current.getString(R.string.login_complete_text)) + } + } + error.Show { + Row(TwoMPadding.base) { + ErrorText(it) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/steffo/twom/login/LoginActivityScaffold.kt b/app/src/main/java/eu/steffo/twom/composables/login/LoginScaffold.kt similarity index 52% rename from app/src/main/java/eu/steffo/twom/login/LoginActivityScaffold.kt rename to app/src/main/java/eu/steffo/twom/composables/login/LoginScaffold.kt index 98c99c4..19f4bb8 100644 --- a/app/src/main/java/eu/steffo/twom/login/LoginActivityScaffold.kt +++ b/app/src/main/java/eu/steffo/twom/composables/login/LoginScaffold.kt @@ -1,9 +1,13 @@ -package eu.steffo.twom.login +package eu.steffo.twom.composables.login +import android.app.Activity +import android.content.Intent +import androidx.activity.ComponentActivity import androidx.compose.foundation.layout.padding import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.Preview import eu.steffo.twom.theme.TwoMTheme import org.matrix.android.sdk.api.session.Session @@ -11,21 +15,26 @@ import org.matrix.android.sdk.api.session.Session @Composable @Preview -fun LoginActivityScaffold( - onBack: () -> Unit = {}, +fun LoginScaffold( onLogin: (session: Session) -> Unit = {}, ) { + val context = LocalContext.current + val activity = context as Activity + + fun submitActivity() { + activity.setResult(ComponentActivity.RESULT_OK, Intent()) + activity.finish() + } + TwoMTheme { Scaffold( topBar = { - LoginActivityTopBar( - onBack = onBack, - ) + LoginActivityTopBar() }, content = { - LoginActivityContent( + LoginForm( modifier = Modifier.padding(it), - onLogin = onLogin + onLogin = { submitActivity() } ) }, ) diff --git a/app/src/main/java/eu/steffo/twom/login/LoginActivityTopBar.kt b/app/src/main/java/eu/steffo/twom/composables/login/LoginTopBar.kt similarity index 53% rename from app/src/main/java/eu/steffo/twom/login/LoginActivityTopBar.kt rename to app/src/main/java/eu/steffo/twom/composables/login/LoginTopBar.kt index 38eb797..67636ec 100644 --- a/app/src/main/java/eu/steffo/twom/login/LoginActivityTopBar.kt +++ b/app/src/main/java/eu/steffo/twom/composables/login/LoginTopBar.kt @@ -1,10 +1,6 @@ -package eu.steffo.twom.login +package eu.steffo.twom.composables.login -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable @@ -12,24 +8,17 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.Preview import eu.steffo.twom.R +import eu.steffo.twom.composables.navigation.BackIconButton @OptIn(ExperimentalMaterial3Api::class) @Composable @Preview fun LoginActivityTopBar( modifier: Modifier = Modifier, - onBack: () -> Unit = {}, ) { TopAppBar( modifier = modifier, - navigationIcon = { - IconButton(onClick = onBack) { - Icon( - imageVector = Icons.Filled.ArrowBack, - contentDescription = LocalContext.current.getString(R.string.back) - ) - } - }, + navigationIcon = { BackIconButton() }, title = { Text(LocalContext.current.getString(R.string.login_title)) } ) } \ No newline at end of file diff --git a/app/src/main/java/eu/steffo/twom/main/MainActivityAccountIconButton.kt b/app/src/main/java/eu/steffo/twom/composables/main/AccountIconButton.kt similarity index 83% rename from app/src/main/java/eu/steffo/twom/main/MainActivityAccountIconButton.kt rename to app/src/main/java/eu/steffo/twom/composables/main/AccountIconButton.kt index 4cfdef9..68cd56f 100644 --- a/app/src/main/java/eu/steffo/twom/main/MainActivityAccountIconButton.kt +++ b/app/src/main/java/eu/steffo/twom/composables/main/AccountIconButton.kt @@ -1,5 +1,7 @@ -package eu.steffo.twom.main +package eu.steffo.twom.composables.main +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.launch import androidx.compose.foundation.layout.Box import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.AccountCircle @@ -18,19 +20,23 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import eu.steffo.twom.R +import eu.steffo.twom.activities.LoginActivity import eu.steffo.twom.matrix.LocalSession import eu.steffo.twom.matrix.avatar.AvatarFromUserId @Composable @Preview(showBackground = true) -fun MainActivityAccountIconButton( +fun AccountIconButton( modifier: Modifier = Modifier, - onClickLogin: () -> Unit = {}, - onClickLogout: () -> Unit = {}, + processLogin: () -> Unit = {}, + processLogout: () -> Unit = {}, ) { val session = LocalSession.current var expanded by remember { mutableStateOf(false) } + val loginLauncher = + rememberLauncherForActivityResult(LoginActivity.Contract()) { processLogin() } + Box(modifier) { IconButton( onClick = { expanded = true }, @@ -58,7 +64,7 @@ fun MainActivityAccountIconButton( }, onClick = { expanded = false - onClickLogin() + loginLauncher.launch() } ) } else { @@ -68,7 +74,7 @@ fun MainActivityAccountIconButton( }, onClick = { expanded = false - onClickLogout() + processLogout() } ) } diff --git a/app/src/main/java/eu/steffo/twom/composables/main/CreateRoomFAB.kt b/app/src/main/java/eu/steffo/twom/composables/main/CreateRoomFAB.kt new file mode 100644 index 0000000..e3c2213 --- /dev/null +++ b/app/src/main/java/eu/steffo/twom/composables/main/CreateRoomFAB.kt @@ -0,0 +1,33 @@ +package eu.steffo.twom.composables.main + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material3.ExtendedFloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import eu.steffo.twom.R + +@Composable +@Preview +fun CreateRoomFAB( + modifier: Modifier = Modifier, + onClick: () -> Unit = {}, +) { + ExtendedFloatingActionButton( + modifier = modifier, + onClick = onClick, + icon = { + Icon( + Icons.Filled.Add, + contentDescription = null + ) + }, + text = { + Text(stringResource(R.string.main_efab_create_text)) + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/eu/steffo/twom/main/MainActivityRoomList.kt b/app/src/main/java/eu/steffo/twom/composables/main/MainContentLoggedIn.kt similarity index 51% rename from app/src/main/java/eu/steffo/twom/main/MainActivityRoomList.kt rename to app/src/main/java/eu/steffo/twom/composables/main/MainContentLoggedIn.kt index 1fba34c..bb2ee84 100644 --- a/app/src/main/java/eu/steffo/twom/main/MainActivityRoomList.kt +++ b/app/src/main/java/eu/steffo/twom/composables/main/MainContentLoggedIn.kt @@ -1,31 +1,32 @@ -package eu.steffo.twom.main +package eu.steffo.twom.composables.main -import android.util.Log import androidx.compose.foundation.layout.Column import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import eu.steffo.twom.R +import eu.steffo.twom.main.RoomListItem import eu.steffo.twom.matrix.LocalSession import eu.steffo.twom.matrix.TwoMMatrix +import eu.steffo.twom.theme.ErrorText import eu.steffo.twom.theme.TwoMPadding -import kotlinx.coroutines.launch import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams @Composable -fun MainActivityRoomList( +fun MainContentLoggedIn( modifier: Modifier = Modifier, - onClickRoom: (roomId: String) -> Unit = {}, ) { - val scope = rememberCoroutineScope() - val session = LocalSession.current - val roomSummaries by session!!.roomService().getRoomSummariesLive( + if (session == null) { + ErrorText(stringResource(R.string.error_session_missing)) + return + } + + val roomSummaries by session.roomService().getRoomSummariesLive( roomSummaryQueryParams { this.memberships = listOf(Membership.JOIN) this.includeType = listOf(TwoMMatrix.ROOM_TYPE) @@ -44,25 +45,7 @@ fun MainActivityRoomList( text = stringResource(R.string.main_roomlist_empty_text) ) } else { - roomSummaries!!.forEach { - RoomListItem( - roomSummary = it, - onClickRoom = onClickRoom, - onLeaveRoom = { - scope.launch LeaveRoom@{ - Log.i("Main", "Leaving room `$it`...") - try { - session!!.roomService().leaveRoom(it, "Decided to leave the room") - } catch (error: Throwable) { - Log.e("Main", "Failed to leave room `$it`: $error") - // TODO: Display an error somewhere - return@LeaveRoom - } - Log.d("Main", "Successfully left room `$it`!") - } - } - ) - } + roomSummaries!!.forEach { RoomListItem(it) } } } } \ No newline at end of file diff --git a/app/src/main/java/eu/steffo/twom/main/MainActivityNotLoggedIn.kt b/app/src/main/java/eu/steffo/twom/composables/main/MainContentNotLoggedIn.kt similarity index 91% rename from app/src/main/java/eu/steffo/twom/main/MainActivityNotLoggedIn.kt rename to app/src/main/java/eu/steffo/twom/composables/main/MainContentNotLoggedIn.kt index 25dc2b4..966dc0b 100644 --- a/app/src/main/java/eu/steffo/twom/main/MainActivityNotLoggedIn.kt +++ b/app/src/main/java/eu/steffo/twom/composables/main/MainContentNotLoggedIn.kt @@ -1,4 +1,4 @@ -package eu.steffo.twom.main +package eu.steffo.twom.composables.main import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -12,7 +12,7 @@ import eu.steffo.twom.theme.TwoMPadding @Composable @Preview(showBackground = true) -fun MainActivityNotLoggedIn( +fun MainContentNotLoggedIn( modifier: Modifier = Modifier, onClickLogin: () -> Unit = {}, ) { diff --git a/app/src/main/java/eu/steffo/twom/main/MainActivityScaffold.kt b/app/src/main/java/eu/steffo/twom/composables/main/MainScaffold.kt similarity index 54% rename from app/src/main/java/eu/steffo/twom/main/MainActivityScaffold.kt rename to app/src/main/java/eu/steffo/twom/composables/main/MainScaffold.kt index ba4f14b..8acc0bd 100644 --- a/app/src/main/java/eu/steffo/twom/main/MainActivityScaffold.kt +++ b/app/src/main/java/eu/steffo/twom/composables/main/MainScaffold.kt @@ -1,4 +1,4 @@ -package eu.steffo.twom.main +package eu.steffo.twom.composables.main import androidx.compose.foundation.layout.padding import androidx.compose.material3.Scaffold @@ -12,32 +12,29 @@ import org.matrix.android.sdk.api.session.Session @Composable @Preview -fun MatrixActivityScaffold( - onClickLogin: () -> Unit = {}, - onClickLogout: () -> Unit = {}, - onClickRoom: (roomId: String) -> Unit = {}, - onClickCreate: () -> Unit = {}, +fun MainScaffold( + processLogin: () -> Unit = {}, + processLogout: () -> Unit = {}, session: Session? = null, ) { TwoMTheme { CompositionLocalProvider(LocalSession provides session) { Scaffold( topBar = { - MainActivityTopBar( - onClickLogin = onClickLogin, - onClickLogout = onClickLogout, + MainTopBar( + processLogin = processLogin, + processLogout = processLogout, ) }, floatingActionButton = { - MainActivityCreateFAB( - onClickCreate = onClickCreate, - ) + CreateRoomFAB() }, content = { - MainActivityContent( - modifier = Modifier.padding(it), - onClickRoom = onClickRoom, - ) + if (session == null) { + MainContentNotLoggedIn(Modifier.padding(it)) + } else { + MainContentLoggedIn(Modifier.padding(it)) + } } ) } diff --git a/app/src/main/java/eu/steffo/twom/main/MainActivityTopBar.kt b/app/src/main/java/eu/steffo/twom/composables/main/MainTopBar.kt similarity index 72% rename from app/src/main/java/eu/steffo/twom/main/MainActivityTopBar.kt rename to app/src/main/java/eu/steffo/twom/composables/main/MainTopBar.kt index 4053102..f6f4973 100644 --- a/app/src/main/java/eu/steffo/twom/main/MainActivityTopBar.kt +++ b/app/src/main/java/eu/steffo/twom/composables/main/MainTopBar.kt @@ -1,4 +1,4 @@ -package eu.steffo.twom.main +package eu.steffo.twom.composables.main import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.ExperimentalMaterial3Api @@ -13,10 +13,10 @@ import eu.steffo.twom.R @OptIn(ExperimentalMaterial3Api::class) @Composable @Preview -fun MainActivityTopBar( +fun MainTopBar( modifier: Modifier = Modifier, - onClickLogin: () -> Unit = {}, - onClickLogout: () -> Unit = {}, + processLogin: () -> Unit = {}, + processLogout: () -> Unit = {}, ) { CenterAlignedTopAppBar( modifier = modifier, @@ -24,9 +24,9 @@ fun MainActivityTopBar( Text(LocalContext.current.getString(R.string.app_name)) }, actions = { - MainActivityAccountIconButton( - onClickLogin = onClickLogin, - onClickLogout = onClickLogout, + AccountIconButton( + processLogin = processLogin, + processLogout = processLogout, ) }, ) diff --git a/app/src/main/java/eu/steffo/twom/create/CreateActivity.kt b/app/src/main/java/eu/steffo/twom/create/CreateActivity.kt deleted file mode 100644 index 6b83e20..0000000 --- a/app/src/main/java/eu/steffo/twom/create/CreateActivity.kt +++ /dev/null @@ -1,39 +0,0 @@ -package eu.steffo.twom.create - -import android.content.Intent -import android.net.Uri -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent - -class CreateActivity : ComponentActivity() { - companion object { - const val NAME_EXTRA = "name" - const val DESCRIPTION_EXTRA = "description" - const val AVATAR_EXTRA = "avatar" - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - setContent { - CreateActivityScaffold( - onClickBack = { - setResult(RESULT_CANCELED) - finish() - }, - onClickCreate = { name: String, description: String, avatarUri: Uri? -> - val resultIntent = Intent() - resultIntent.putExtra(NAME_EXTRA, name) - resultIntent.putExtra(DESCRIPTION_EXTRA, description) - // Kotlin cannot use nullable types in Java interop generics - if (avatarUri != null) { - resultIntent.putExtra(AVATAR_EXTRA, avatarUri) - } - setResult(RESULT_OK, resultIntent) - finish() - }, - ) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/eu/steffo/twom/create/CreateActivityScaffold.kt b/app/src/main/java/eu/steffo/twom/create/CreateActivityScaffold.kt deleted file mode 100644 index 10b4169..0000000 --- a/app/src/main/java/eu/steffo/twom/create/CreateActivityScaffold.kt +++ /dev/null @@ -1,32 +0,0 @@ -package eu.steffo.twom.create - -import android.net.Uri -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Scaffold -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import eu.steffo.twom.theme.TwoMTheme - -@Composable -@Preview -fun CreateActivityScaffold( - onClickBack: () -> Unit = {}, - onClickCreate: (name: String, description: String, avatarUri: Uri?) -> Unit = { _, _, _ -> }, -) { - TwoMTheme { - Scaffold( - topBar = { - CreateActivityTopBar( - onClickBack = onClickBack, - ) - }, - content = { - CreateActivityContent( - modifier = Modifier.padding(it), - onClickCreate = onClickCreate, - ) - } - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/eu/steffo/twom/login/ErrorText.kt b/app/src/main/java/eu/steffo/twom/login/ErrorText.kt deleted file mode 100644 index 9381d27..0000000 --- a/app/src/main/java/eu/steffo/twom/login/ErrorText.kt +++ /dev/null @@ -1,58 +0,0 @@ -package eu.steffo.twom.login - -import androidx.compose.material3.LocalTextStyle -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.TextLayoutResult -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.font.FontStyle -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextDecoration -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.TextUnit - -@Composable -@Preview(showBackground = true) -fun ErrorText( - modifier: Modifier = Modifier, - text: String = "", - fontSize: TextUnit = TextUnit.Unspecified, - fontStyle: FontStyle? = null, - fontWeight: FontWeight? = null, - fontFamily: FontFamily? = null, - letterSpacing: TextUnit = TextUnit.Unspecified, - textDecoration: TextDecoration? = null, - textAlign: TextAlign? = null, - lineHeight: TextUnit = TextUnit.Unspecified, - overflow: TextOverflow = TextOverflow.Clip, - softWrap: Boolean = true, - maxLines: Int = Int.MAX_VALUE, - minLines: Int = 1, - onTextLayout: (TextLayoutResult) -> Unit = {}, - style: TextStyle = LocalTextStyle.current -) { - Text( - modifier = modifier, - text = text, - color = MaterialTheme.colorScheme.error, - fontSize = fontSize, - fontStyle = fontStyle, - fontWeight = fontWeight, - fontFamily = fontFamily, - letterSpacing = letterSpacing, - textDecoration = textDecoration, - textAlign = textAlign, - lineHeight = lineHeight, - overflow = overflow, - softWrap = softWrap, - maxLines = maxLines, - minLines = minLines, - onTextLayout = onTextLayout, - style = style, - ) -} \ No newline at end of file diff --git a/app/src/main/java/eu/steffo/twom/login/LoginActivity.kt b/app/src/main/java/eu/steffo/twom/login/LoginActivity.kt deleted file mode 100644 index faf97f8..0000000 --- a/app/src/main/java/eu/steffo/twom/login/LoginActivity.kt +++ /dev/null @@ -1,24 +0,0 @@ -package eu.steffo.twom.login - -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent - - -class LoginActivity : ComponentActivity() { - override fun onStart() { - super.onStart() - - setContent { - LoginActivityScaffold( - onBack = { - setResult(RESULT_CANCELED) - finish() - }, - onLogin = { - setResult(RESULT_OK) - finish() - }, - ) - } - } -} diff --git a/app/src/main/java/eu/steffo/twom/login/LoginActivityContent.kt b/app/src/main/java/eu/steffo/twom/login/LoginActivityContent.kt deleted file mode 100644 index 900d04c..0000000 --- a/app/src/main/java/eu/steffo/twom/login/LoginActivityContent.kt +++ /dev/null @@ -1,226 +0,0 @@ -package eu.steffo.twom.login - -import android.util.Log -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.material3.Button -import androidx.compose.material3.LinearProgressIndicator -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ProgressIndicatorDefaults -import androidx.compose.material3.Text -import androidx.compose.material3.TextField -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import eu.steffo.twom.R -import eu.steffo.twom.matrix.TwoMMatrix -import eu.steffo.twom.theme.TwoMPadding -import kotlinx.coroutines.launch -import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig -import org.matrix.android.sdk.api.auth.data.LoginFlowResult -import org.matrix.android.sdk.api.auth.login.LoginWizard -import org.matrix.android.sdk.api.auth.wellknown.WellknownResult -import org.matrix.android.sdk.api.failure.MatrixIdFailure -import org.matrix.android.sdk.api.session.Session - - -// TODO: Localize error messages -@Composable -@Preview(showBackground = true) -fun LoginActivityContent( - modifier: Modifier = Modifier, - onLogin: (session: Session) -> Unit = {}, -) { - val scope = rememberCoroutineScope() - - var username by rememberSaveable { mutableStateOf("") } - var password by rememberSaveable { mutableStateOf("") } - - var loginStep by rememberSaveable { mutableStateOf(LoginStep.NONE) } - var errorMessageId by rememberSaveable { mutableStateOf(null) } - var errorMessageString by rememberSaveable { mutableStateOf(null) } - - Column(modifier) { - LinearProgressIndicator( - modifier = Modifier.fillMaxWidth(), - progress = loginStep.step.toFloat() / LoginStep.DONE.step.toFloat(), - color = if (errorMessageId != null || errorMessageString != null) MaterialTheme.colorScheme.error else ProgressIndicatorDefaults.linearColor - ) - Row(TwoMPadding.base) { - Text(LocalContext.current.getString(R.string.login_text)) - } - Row(TwoMPadding.base) { - TextField( - modifier = Modifier.fillMaxWidth(), - singleLine = true, - value = username, - onValueChange = { username = it }, - label = { - Text(LocalContext.current.getString(R.string.login_username_label)) - }, - placeholder = { - Text(LocalContext.current.getString(R.string.login_username_placeholder)) - }, - supportingText = { - Text(LocalContext.current.getString(R.string.login_username_supporting)) - }, - ) - } - Row(TwoMPadding.base) { - PasswordField( - modifier = Modifier.fillMaxWidth(), - singleLine = true, - value = password, - onValueChange = { password = it }, - label = { - Text(LocalContext.current.getString(R.string.login_password_label)) - }, - placeholder = { - Text(LocalContext.current.getString(R.string.login_password_placeholder)) - }, - supportingText = { - Text(LocalContext.current.getString(R.string.login_password_supporting)) - }, - ) - } - Row(TwoMPadding.base) { - Button( - modifier = Modifier.fillMaxWidth(), - onClick = { - scope.launch Login@{ - errorMessageId = null - errorMessageString = null - - Log.d("Login", "Getting authentication service...") - loginStep = LoginStep.SERVICE - val auth = TwoMMatrix.matrix.authenticationService() - - Log.d("Login", "Resetting authentication service...") - auth.reset() - - Log.d("Login", "Retrieving .well-known data for: $username") - loginStep = LoginStep.WELLKNOWN - lateinit var wellKnown: WellknownResult - try { - wellKnown = auth.getWellKnownData(username, null) - } catch (e: MatrixIdFailure.InvalidMatrixId) { - Log.d( - "Login", - "User seems to have input an invalid Matrix ID: $username", - e - ) - errorMessageId = R.string.login_error_username_invalid - return@Login - } catch (e: Throwable) { - // TODO: It sure would be nice to know which exceptions can be thrown here. - Log.e( - "Login", - "Something went wrong while retrieving .well-known data for: $username", - e - ) - errorMessageString = e.toString() - return@Login - } - if (wellKnown !is WellknownResult.Prompt) { - Log.w( - "Login", - "Data is not .well-known for: $username" - ) - errorMessageId = R.string.login_error_wellknown_missing - return@Login - } - - Log.d("Login", "Retrieving login flows for: ${wellKnown.homeServerUrl}") - loginStep = LoginStep.FLOWS - lateinit var flows: LoginFlowResult - try { - flows = auth.getLoginFlow( - HomeServerConnectionConfig - .Builder() - .withHomeServerUri(wellKnown.homeServerUrl) - .build() - ) - } catch (e: Throwable) { - // TODO: It sure would be nice to know which exceptions can be thrown here. - Log.e( - "Login", - "Something went wrong while retrieving login flows for: ${wellKnown.homeServerUrl}", - e - ) - errorMessageString = e.message - return@Login - } - - Log.d("Login", "Creating login wizard...") - loginStep = LoginStep.WIZARD - lateinit var wizard: LoginWizard - try { - wizard = auth.getLoginWizard() // Why is this stateful? Aargh. - } catch (e: Throwable) { - // TODO: It sure would be nice to know which exceptions can be thrown here. - Log.e( - "Login", - "Something went wrong while creating the login wizard.", - e - ) - errorMessageString = e.message - return@Login - } - - Log.d("Login", "Logging in as: $username") - loginStep = LoginStep.LOGIN - lateinit var session: Session - try { - session = wizard.login( - login = username, - password = password, - initialDeviceName = "TwoM (Android)", - ) - } catch (e: Throwable) { - Log.e( - "Login", - "Something went wrong while logging in as: $username", - e - ) - errorMessageString = e.message - return@Login - } - - Log.d( - "Login", - "Logged in successfully with session id: ${session.sessionId}" - ) - loginStep = LoginStep.DONE - - onLogin(session) - } - }, - enabled = (username != "" && (loginStep == LoginStep.NONE || errorMessageId != null)), - ) { - Text(LocalContext.current.getString(R.string.login_complete_text)) - } - } - if (errorMessageId != null) { - Row(TwoMPadding.base) { - // FIXME: Can this cause an error? - ErrorText( - text = if (errorMessageString != null) { - errorMessageString!! - } else if (errorMessageId != null) { - stringResource(errorMessageId!!) - } else { - "Unknown error" - } - ) - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/eu/steffo/twom/login/LoginStep.kt b/app/src/main/java/eu/steffo/twom/login/LoginStep.kt deleted file mode 100644 index dbf9139..0000000 --- a/app/src/main/java/eu/steffo/twom/login/LoginStep.kt +++ /dev/null @@ -1,11 +0,0 @@ -package eu.steffo.twom.login - -enum class LoginStep(val step: Int) { - NONE(0), - SERVICE(1), - WELLKNOWN(2), - FLOWS(3), - WIZARD(4), - LOGIN(5), - DONE(6), -} \ No newline at end of file diff --git a/app/src/main/java/eu/steffo/twom/main/MainActivityContent.kt b/app/src/main/java/eu/steffo/twom/main/MainActivityContent.kt deleted file mode 100644 index 3e1260e..0000000 --- a/app/src/main/java/eu/steffo/twom/main/MainActivityContent.kt +++ /dev/null @@ -1,24 +0,0 @@ -package eu.steffo.twom.main - -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import eu.steffo.twom.matrix.LocalSession - -@Composable -fun MainActivityContent( - modifier: Modifier = Modifier, - onClickRoom: (roomId: String) -> Unit = {}, -) { - val session = LocalSession.current - - if (session == null) { - MainActivityNotLoggedIn( - modifier = modifier, - ) - } else { - MainActivityRoomList( - modifier = modifier, - onClickRoom = onClickRoom, - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/eu/steffo/twom/main/MainActivityCreateFAB.kt b/app/src/main/java/eu/steffo/twom/main/MainActivityCreateFAB.kt deleted file mode 100644 index fc45876..0000000 --- a/app/src/main/java/eu/steffo/twom/main/MainActivityCreateFAB.kt +++ /dev/null @@ -1,41 +0,0 @@ -package eu.steffo.twom.main - -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Add -import androidx.compose.material3.ExtendedFloatingActionButton -import androidx.compose.material3.Icon -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalView -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import eu.steffo.twom.R -import eu.steffo.twom.matrix.LocalSession - -@Composable -@Preview -fun MainActivityCreateFAB( - modifier: Modifier = Modifier, - onClickCreate: () -> Unit = {}, -) { - val session = LocalSession.current - - val shouldDisplay = (session != null || LocalView.current.isInEditMode) - - if (shouldDisplay) { - ExtendedFloatingActionButton( - modifier = modifier, - onClick = onClickCreate, - icon = { - Icon( - Icons.Filled.Add, - contentDescription = null - ) - }, - text = { - Text(stringResource(R.string.main_efab_create_text)) - } - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/eu/steffo/twom/main/RoomListItem.kt b/app/src/main/java/eu/steffo/twom/main/RoomListItem.kt index 57fa17b..79584e2 100644 --- a/app/src/main/java/eu/steffo/twom/main/RoomListItem.kt +++ b/app/src/main/java/eu/steffo/twom/main/RoomListItem.kt @@ -1,5 +1,7 @@ package eu.steffo.twom.main +import android.util.Log +import androidx.activity.compose.rememberLauncherForActivityResult import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Box @@ -12,6 +14,8 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier @@ -19,7 +23,12 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import eu.steffo.twom.R +import eu.steffo.twom.activities.RoomActivity +import eu.steffo.twom.composables.errorhandling.LocalizableError +import eu.steffo.twom.matrix.LocalSession import eu.steffo.twom.matrix.avatar.AvatarFromURL +import eu.steffo.twom.theme.ErrorText +import kotlinx.coroutines.launch import org.matrix.android.sdk.api.session.room.model.RoomSummary @@ -27,14 +36,42 @@ import org.matrix.android.sdk.api.session.room.model.RoomSummary @Composable fun RoomListItem( roomSummary: RoomSummary, - onClickRoom: (roomId: String) -> Unit = {}, - onLeaveRoom: (roomId: String) -> Unit = {}, ) { + val roomId = roomSummary.roomId + + val scope = rememberCoroutineScope() + + val session = LocalSession.current + if (session == null) { + ErrorText(stringResource(R.string.error_session_missing)) + return + } + var expanded by rememberSaveable { mutableStateOf(false) } + val error by remember { mutableStateOf(LocalizableError()) } + + val roomActivityLauncher = rememberLauncherForActivityResult(RoomActivity.Contract()) {} + + fun openRoom() { + Log.i("Main", "Opening room `$roomId`...") + roomActivityLauncher.launch(roomId) + } + + suspend fun leaveRoom() { + Log.i("Main", "Leaving room `$roomId`...") + try { + session.roomService().leaveRoom(roomId, "Decided to leave the room") + } catch (e: Throwable) { + Log.e("Main", "Failed to leave room `$roomId`: $error") + error.set(R.string.main_error_leave_generic, e) + return + } + Log.d("Main", "Successfully left room `$roomId`!") + } ListItem( modifier = Modifier.combinedClickable( - onClick = { onClickRoom(roomSummary.roomId) }, + onClick = { openRoom() }, onLongClick = { expanded = true } ), headlineContent = { @@ -72,7 +109,7 @@ fun RoomListItem( }, onClick = { expanded = false - onLeaveRoom(roomSummary.roomId) + scope.launch { leaveRoom() } } ) } diff --git a/app/src/main/java/eu/steffo/twom/room/RoomActivityContent.kt b/app/src/main/java/eu/steffo/twom/room/RoomActivityContent.kt index 99b7493..f5ee28d 100644 --- a/app/src/main/java/eu/steffo/twom/room/RoomActivityContent.kt +++ b/app/src/main/java/eu/steffo/twom/room/RoomActivityContent.kt @@ -34,7 +34,7 @@ fun RoomActivityContent( val session = LocalSession.current if (session == null) { - ErrorText(stringResource(R.string.room_error_session_missing)) + ErrorText(stringResource(R.string.error_session_missing)) return } diff --git a/app/src/main/java/eu/steffo/twom/create/ImageHandler.kt b/app/src/main/java/eu/steffo/twom/utils/BitmapUtilities.kt similarity index 98% rename from app/src/main/java/eu/steffo/twom/create/ImageHandler.kt rename to app/src/main/java/eu/steffo/twom/utils/BitmapUtilities.kt index a3fb606..50a0b9e 100644 --- a/app/src/main/java/eu/steffo/twom/create/ImageHandler.kt +++ b/app/src/main/java/eu/steffo/twom/utils/BitmapUtilities.kt @@ -1,4 +1,4 @@ -package eu.steffo.twom.create +package eu.steffo.twom.utils import android.content.ContentResolver import android.graphics.Bitmap @@ -8,7 +8,7 @@ import android.net.Uri import androidx.exifinterface.media.ExifInterface import java.io.File -class ImageHandler { +class BitmapUtilities { companion object { fun getOrientation(contentResolver: ContentResolver, uri: Uri): Int? { contentResolver.openInputStream(uri).use { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ab214e0..6ccae6c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -55,7 +55,7 @@ Hasn\'t answered yet Leave a comment… Update - The Matrix session context has not been initialized. + The Matrix session context has not been initialized. Could not find the requested Matrix room. The Matrix room context has not been initialized. No answer @@ -69,4 +69,9 @@ Not opened Hasn\'t opened the invite yet Leave party + Something went wrong while retrieving .well-known data: %1$s + Something went wrong while retrieving login flows: %1$s + Something went wrong while setting up the login wizard: %1$s + Something went wrong while logging in: %1$s + Something went wrong while leaving the room: %1$s \ No newline at end of file diff --git a/app/src/test/java/eu/steffo/twom/ExampleUnitTest.kt b/app/src/test/java/eu/steffo/twom/ExampleUnitTest.kt deleted file mode 100644 index cf9f700..0000000 --- a/app/src/test/java/eu/steffo/twom/ExampleUnitTest.kt +++ /dev/null @@ -1,17 +0,0 @@ -package eu.steffo.twom - -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) - } -} \ No newline at end of file