1
Fork 0
mirror of https://github.com/Steffo99/twom.git synced 2024-11-21 15:44:26 +00:00

Start some kind of massive refactor

This commit is contained in:
Steffo 2024-01-10 13:50:30 +01:00
parent 59932ab67c
commit 5befff9864
Signed by: steffo
GPG key ID: 2A24051445686895
34 changed files with 602 additions and 671 deletions

View file

@ -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)
}
}

View file

@ -31,7 +31,7 @@
</provider>
<activity
android:name=".main.MainActivity"
android:name=".activities.MainActivity"
android:exported="true"
android:theme="@style/Theme.TwoM">
<intent-filter>
@ -42,15 +42,15 @@
</activity>
<activity
android:name=".login.LoginActivity"
android:name=".activities.LoginActivity"
android:theme="@style/Theme.TwoM" />
<activity
android:name=".room.RoomActivity"
android:name=".activities.RoomActivity"
android:theme="@style/Theme.TwoM" />
<activity
android:name=".create.CreateActivity"
android:name=".activities.CreateRoomActivity"
android:theme="@style/Theme.TwoM" />
</application>

View file

@ -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<Unit, CreateRoomActivityResult?>() {
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() }
}
}

View file

@ -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<Unit, Unit>() {
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() }
}
}

View file

@ -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<Uri>(CreateActivity.AVATAR_EXTRA)
intent.getParcelableExtra<Uri>(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!")
},
)
}
}

View file

@ -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<String, Unit>() {
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() {

View file

@ -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,
)
}
}

View file

@ -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<Uri?>(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))

View file

@ -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)
}
)
}
)
}
}

View file

@ -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)) }
)
}

View file

@ -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,
)
}

View file

@ -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

View file

@ -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)
}
}
}
}

View file

@ -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() }
)
},
)

View file

@ -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)) }
)
}

View file

@ -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()
}
)
}

View file

@ -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))
}
)
}

View file

@ -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) }
}
}
}

View file

@ -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 = {},
) {

View file

@ -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))
}
}
)
}

View file

@ -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,
)
},
)

View file

@ -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()
},
)
}
}
}

View file

@ -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,
)
}
)
}
}

View file

@ -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,
)
}

View file

@ -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()
},
)
}
}
}

View file

@ -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<Int?>(null) }
var errorMessageString by rememberSaveable { mutableStateOf<String?>(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"
}
)
}
}
}
}

View file

@ -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),
}

View file

@ -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,
)
}
}

View file

@ -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))
}
)
}
}

View file

@ -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() }
}
)
}

View file

@ -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
}

View file

@ -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 {

View file

@ -55,7 +55,7 @@
<string name="room_rsvp_unknown_response">Hasn\'t answered yet</string>
<string name="room_rsvp_unknown_placeholder">Leave a comment…</string>
<string name="room_update_label">Update</string>
<string name="room_error_session_missing">The Matrix session context has not been initialized.</string>
<string name="error_session_missing">The Matrix session context has not been initialized.</string>
<string name="room_error_room_notfound">Could not find the requested Matrix room.</string>
<string name="room_error_room_missing">The Matrix room context has not been initialized.</string>
<string name="room_rsvp_unknown_label">No answer</string>
@ -69,4 +69,9 @@
<string name="room_rsvp_invited_label">Not opened</string>
<string name="room_rsvp_invited_response">Hasn\'t opened the invite yet</string>
<string name="main_room_leave_label">Leave party</string>
<string name="login_error_wellknown_generic">Something went wrong while retrieving .well-known data: %1$s</string>
<string name="login_error_flows_generic">Something went wrong while retrieving login flows: %1$s</string>
<string name="login_error_wizard_generic">Something went wrong while setting up the login wizard: %1$s</string>
<string name="login_error_login_generic">Something went wrong while logging in: %1$s</string>
<string name="main_error_leave_generic">Something went wrong while leaving the room: %1$s</string>
</resources>

View file

@ -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)
}
}