1
Fork 0
mirror of https://github.com/Steffo99/twom.git synced 2024-11-25 01:24:24 +00:00

Allow changes to the room configuration

This commit is contained in:
Steffo 2024-01-20 21:43:32 +01:00
parent fb45b3ea06
commit 395709cc28
Signed by: steffo
GPG key ID: 5ADA3868646C3FC0
11 changed files with 231 additions and 102 deletions

View file

@ -17,32 +17,68 @@ class ConfigureRoomActivity : ComponentActivity() {
const val AVATAR_EXTRA = "avatar"
}
data class Result(
data class Configuration(
val name: String,
val description: String,
val avatarUri: Uri?,
)
) {
fun toIntent(): Intent {
val intent = Intent()
intent.putExtra(NAME_EXTRA, this.name)
intent.putExtra(DESCRIPTION_EXTRA, this.description)
if (this.avatarUri != null) {
intent.putExtra(AVATAR_EXTRA, this.avatarUri.toString())
}
return intent
}
class Contract : ActivityResultContract<Unit, Result?>() {
fun <A> toIntent(context: Context, klass: Class<A>): Intent {
val intent = Intent(context, klass)
intent.putExtra(NAME_EXTRA, this.name)
intent.putExtra(DESCRIPTION_EXTRA, this.description)
if (this.avatarUri != null) {
intent.putExtra(AVATAR_EXTRA, this.avatarUri.toString())
}
return intent
}
companion object {
fun fromIntent(intent: Intent): Configuration? {
val name = intent.getStringExtra(NAME_EXTRA) ?: return null
val description = intent.getStringExtra(DESCRIPTION_EXTRA) ?: return null
val avatarString = intent.getStringExtra(AVATAR_EXTRA)
val avatarUri = if (avatarString != null) Uri.parse(avatarString) else null
return Configuration(
name = name,
description = description,
avatarUri = avatarUri,
)
}
}
}
class CreateContract : ActivityResultContract<Unit, Configuration?>() {
override fun createIntent(context: Context, input: Unit): Intent {
return Intent(context, ConfigureRoomActivity::class.java)
}
override fun parseResult(resultCode: Int, intent: Intent?): Result? {
override fun parseResult(resultCode: Int, intent: Intent?): Configuration? {
return when (resultCode) {
RESULT_OK -> {
intent!!
val name = intent.getStringExtra(NAME_EXTRA)!!
val description = intent.getStringExtra(DESCRIPTION_EXTRA)!!
val avatar = intent.getStringExtra(AVATAR_EXTRA)
Result(
name = name,
description = description,
avatarUri = if (avatar != null) Uri.parse(avatar) else null,
)
RESULT_OK -> Configuration.fromIntent(intent!!)
else -> null
}
}
}
class EditContract : ActivityResultContract<Configuration, Configuration?>() {
override fun createIntent(context: Context, input: Configuration): Intent {
return input.toIntent(context, ConfigureRoomActivity::class.java)
}
override fun parseResult(resultCode: Int, intent: Intent?): Configuration? {
return when (resultCode) {
RESULT_OK -> Configuration.fromIntent(intent!!)
else -> null
}
}
@ -51,6 +87,12 @@ class ConfigureRoomActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent { ConfigureRoomScaffold() }
val configuration = Configuration.fromIntent(intent)
setContent {
ConfigureRoomScaffold(
initialConfiguration = configuration,
)
}
}
}

View file

@ -100,8 +100,12 @@ class MainActivity : ComponentActivity() {
Log.d("Main", "Done logging out!")
},
processCreate = { name, description, avatarUri ->
processCreate = {
lifecycleScope.launch {
val name = it.name
val description = it.description
val avatarUri = it.avatarUri
val currentSession = session
val createRoomParams = CreateRoomParams()

View file

@ -4,32 +4,29 @@ import android.graphics.Bitmap
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.clickable
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
import eu.steffo.twom.utils.BitmapUtilities
@OptIn(ExperimentalFoundationApi::class)
@Composable
@Preview(widthDp = 40, heightDp = 40)
fun AvatarPicker(
modifier: Modifier = Modifier,
fallbackText: String = "?",
onPick: (bitmap: Bitmap) -> Unit = {},
value: Bitmap? = null,
onPick: (bitmap: Bitmap?) -> Unit = {},
alpha: Float = 1.0f,
) {
val context = LocalContext.current
val resolver = context.contentResolver
var selection by remember { mutableStateOf<Bitmap?>(null) }
val launcher =
rememberLauncherForActivityResult(ActivityResultContracts.PickVisualMedia()) ImageSelect@{
it ?: return@ImageSelect
@ -39,18 +36,18 @@ fun AvatarPicker(
val correctedBitmap = BitmapUtilities.squareAndOrient(rawBitmap, orientation)
selection = correctedBitmap
onPick(correctedBitmap)
}
Box(
modifier = modifier
.clickable {
launcher.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly))
}
.combinedClickable(
onClick = { launcher.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)) },
onLongClick = { onPick(null) },
)
) {
AvatarImage(
bitmap = selection?.asImageBitmap(),
bitmap = value?.asImageBitmap(),
fallbackText = fallbackText,
alpha = alpha,
)

View file

@ -1,6 +1,9 @@
package eu.steffo.twom.composables.configureroom
import android.app.Activity
import android.graphics.Bitmap
import android.net.Uri
import androidx.activity.ComponentActivity
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
@ -14,16 +17,19 @@ 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.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.contentDescription
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.activities.ConfigureRoomActivity
import eu.steffo.twom.composables.avatar.AvatarPicker
import eu.steffo.twom.composables.theme.basePadding
import eu.steffo.twom.utils.BitmapUtilities
@ -32,11 +38,16 @@ import eu.steffo.twom.utils.BitmapUtilities
@Preview(showBackground = true)
fun ConfigureRoomForm(
modifier: Modifier = Modifier,
onSubmit: (name: String, description: String, avatarUri: String?) -> Unit = { _, _, _ -> },
initialConfiguration: ConfigureRoomActivity.Configuration? = null,
onSubmit: (ConfigureRoomActivity.Configuration) -> Unit = {},
) {
var name by rememberSaveable { mutableStateOf("") }
var description by rememberSaveable { mutableStateOf("") }
var avatarUri by rememberSaveable { mutableStateOf<Uri?>(null) }
var name by rememberSaveable { mutableStateOf(initialConfiguration?.name ?: "") }
var description by rememberSaveable { mutableStateOf(initialConfiguration?.description ?: "") }
// TODO: How to load the original avatar from the URL?
var avatarBitmap by remember { mutableStateOf<Bitmap?>(null) }
val context = LocalContext.current
val activity = context as Activity
Column(modifier) {
Row(Modifier.basePadding()) {
@ -48,10 +59,8 @@ fun ConfigureRoomForm(
.semantics {
this.contentDescription = avatarContentDescription
},
onPick = {
val cache = BitmapUtilities.bitmapToCache("createAvatar", it)
avatarUri = Uri.fromFile(cache)
},
value = avatarBitmap,
onPick = { avatarBitmap = it },
)
TextField(
modifier = Modifier
@ -85,7 +94,19 @@ fun ConfigureRoomForm(
modifier = Modifier
.fillMaxWidth(),
onClick = {
onSubmit(name, description, avatarUri.toString())
val result = ConfigureRoomActivity.Configuration(
name = name,
description = description,
avatarUri = if (avatarBitmap != null) {
Uri.fromFile(
BitmapUtilities.bitmapToCache("createAvatar", avatarBitmap!!)
)
} else {
null
},
)
activity.setResult(ComponentActivity.RESULT_OK, result.toIntent())
activity.finish()
},
) {
Text(stringResource(R.string.create_complete_text))

View file

@ -1,46 +1,29 @@
package eu.steffo.twom.composables.configureroom
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.activities.ConfigureRoomActivity
import eu.steffo.twom.composables.theme.TwoMTheme
@Composable
@Preview
fun ConfigureRoomScaffold() {
val context = LocalContext.current
val activity = context as Activity
fun submitActivity(name: String, description: String, avatarUri: String?) {
val resultIntent = Intent()
resultIntent.putExtra(ConfigureRoomActivity.NAME_EXTRA, name)
resultIntent.putExtra(ConfigureRoomActivity.DESCRIPTION_EXTRA, description)
// Kotlin cannot use nullable types in Java interop generics
if (avatarUri != null) {
resultIntent.putExtra(ConfigureRoomActivity.AVATAR_EXTRA, avatarUri)
}
activity.setResult(ComponentActivity.RESULT_OK, resultIntent)
activity.finish()
}
fun ConfigureRoomScaffold(
initialConfiguration: ConfigureRoomActivity.Configuration? = null,
) {
TwoMTheme {
Scaffold(
topBar = {
ConfigureActivityTopBar()
ConfigureActivityTopBar(
initialName = initialConfiguration?.name,
)
},
content = {
ConfigureRoomForm(
modifier = Modifier.padding(it),
onSubmit = { name: String, description: String, avatarUri: String? ->
submitActivity(name, description, avatarUri)
}
initialConfiguration = initialConfiguration,
)
}
)

View file

@ -5,7 +5,7 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
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.composables.navigation.BackIconButton
@ -15,10 +15,19 @@ import eu.steffo.twom.composables.navigation.BackIconButton
@Preview
fun ConfigureActivityTopBar(
modifier: Modifier = Modifier,
initialName: String? = null,
) {
TopAppBar(
modifier = modifier,
navigationIcon = { BackIconButton() },
title = { Text(LocalContext.current.getString(R.string.create_title)) }
title = {
Text(
text = if (initialName == null) {
stringResource(R.string.create_title)
} else {
stringResource(R.string.edit_title, initialName)
}
)
}
)
}

View file

@ -1,6 +1,5 @@
package eu.steffo.twom.composables.main
import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.launch
import androidx.compose.material.icons.Icons
@ -19,12 +18,12 @@ import eu.steffo.twom.activities.ConfigureRoomActivity
@Preview
fun CreateRoomFAB(
modifier: Modifier = Modifier,
onCreateParamsSelected: (name: String, description: String, avatarUri: Uri?) -> Unit = { _, _, _ -> },
onCreateConfigured: (configuration: ConfigureRoomActivity.Configuration) -> Unit = {},
) {
val launcher =
rememberLauncherForActivityResult(ConfigureRoomActivity.Contract()) {
rememberLauncherForActivityResult(ConfigureRoomActivity.CreateContract()) {
if (it != null) {
onCreateParamsSelected(it.name, it.description, it.avatarUri)
onCreateConfigured(it)
}
}

View file

@ -1,12 +1,12 @@
package eu.steffo.twom.composables.main
import android.net.Uri
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import eu.steffo.twom.activities.ConfigureRoomActivity
import eu.steffo.twom.composables.matrix.LocalSession
import eu.steffo.twom.composables.theme.TwoMTheme
import org.matrix.android.sdk.api.session.Session
@ -16,7 +16,7 @@ import org.matrix.android.sdk.api.session.Session
fun MainScaffold(
processLogin: () -> Unit = {},
processLogout: () -> Unit = {},
processCreate: (name: String, description: String, avatarUri: Uri?) -> Unit = { _, _, _ -> },
processCreate: (it: ConfigureRoomActivity.Configuration) -> Unit = {},
session: Session? = null,
) {
TwoMTheme {
@ -31,7 +31,7 @@ fun MainScaffold(
floatingActionButton = {
if (session != null) {
CreateRoomFAB(
onCreateParamsSelected = processCreate,
onCreateConfigured = processCreate,
)
}
},

View file

@ -1,6 +1,10 @@
package eu.steffo.twom.composables.viewroom
import android.net.Uri
import android.util.Log
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.compose.foundation.layout.Box
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.IconButton
@ -9,27 +13,115 @@ 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.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.core.net.toFile
import eu.steffo.twom.R
import eu.steffo.twom.activities.ConfigureRoomActivity
import eu.steffo.twom.composables.avatar.AvatarURL
import eu.steffo.twom.composables.errorhandling.ErrorIconButton
import eu.steffo.twom.composables.matrix.LocalSession
import kotlinx.coroutines.launch
import kotlin.jvm.optionals.getOrNull
@Composable
fun RoomIconButton(
modifier: Modifier = Modifier,
avatarUrl: String? = null,
canEdit: Boolean = true,
) {
var expanded by remember { mutableStateOf(false) }
val session = LocalSession.current
if (session == null) {
ErrorIconButton(
message = stringResource(R.string.error_session_missing)
)
return
}
val roomSummaryRequest = LocalRoomSummary.current
if (roomSummaryRequest == null) {
CircularProgressIndicator()
return
}
val roomSummary = roomSummaryRequest.getOrNull()
if (roomSummary == null) {
ErrorIconButton(
message = stringResource(R.string.room_error_roomsummary_notfound)
)
return
}
val roomRequest = LocalRoom.current
if (roomRequest == null) {
CircularProgressIndicator()
return
}
val room = roomRequest.getOrNull()
if (room == null) {
ErrorIconButton(
message = stringResource(R.string.room_error_room_notfound)
)
return
}
val scope = rememberCoroutineScope()
val launcher =
rememberLauncherForActivityResult(
ConfigureRoomActivity.EditContract()
) {
if (it != null) {
scope.launch {
val name = it.name
Log.d("RoomIconButton", "Updating room name to `$name`")
room.stateService().updateName(it.name)
val description = it.description
Log.d("RoomIconButton", "Updating room description to `$description`")
room.stateService().updateTopic(it.description)
val avatarUri = it.avatarUri
val avatarFile = avatarUri?.toFile()
when (avatarFile?.isFile) {
false -> {
Log.e(
"RoomIconButton",
"Avatar has been deleted from cache before room could possibly be updated, ignoring..."
)
}
true -> {
Log.d(
"RoomIconButton",
"Avatar seems to exist at: $avatarUri"
)
room.stateService().updateAvatar(avatarUri, avatarFile.name)
}
null -> {
Log.d(
"RoomIconButton",
"Avatar was not set, ignoring..."
)
}
}
}
}
}
Box(modifier) {
IconButton(
onClick = { expanded = true },
) {
AvatarURL(
url = avatarUrl,
url = roomSummary.avatarUrl,
contentDescription = LocalContext.current.getString(R.string.room_options_label),
)
}
@ -37,28 +129,23 @@ fun RoomIconButton(
expanded = expanded,
onDismissRequest = { expanded = false },
) {
DropdownMenuItem(
text = {
Text(stringResource(R.string.room_options_zoom_text))
},
onClick = {
// TODO
expanded = false
}
)
if (canEdit) {
DropdownMenuItem(
text = {
Text(stringResource(R.string.room_options_edit_text))
},
onClick = {
// TODO
expanded = false
launcher.launch(
ConfigureRoomActivity.Configuration(
name = roomSummary.name,
description = roomSummary.topic,
avatarUri = Uri.parse(roomSummary.avatarUrl),
)
)
}
)
}
}
}
}

View file

@ -1,16 +1,12 @@
package eu.steffo.twom.composables.viewroom
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
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
import eu.steffo.twom.composables.errorhandling.ErrorIconButton
import eu.steffo.twom.composables.errorhandling.ErrorText
import eu.steffo.twom.composables.errorhandling.LoadingText
import eu.steffo.twom.composables.navigation.BackIconButton
@ -47,17 +43,7 @@ fun ViewRoomTopBar(
}
},
actions = {
if (roomSummaryRequest == null) {
CircularProgressIndicator()
} else if (roomSummary == null) {
ErrorIconButton(
message = stringResource(R.string.room_error_roomsummary_notfound)
)
} else {
RoomIconButton(
avatarUrl = roomSummary.avatarUrl,
)
}
RoomIconButton()
},
)
}

View file

@ -84,4 +84,5 @@
<string name="room_error_redact_generic">Your response has been updated, but something went wrong while attempting to remove your previous one: %1$s</string>
<string name="room_error_self_notfound">You have been removed from the room.</string>
<string name="room_error_invite_generic">Something went wrong while sending the invite: %1$s</string>
<string name="edit_title">Editing %1$s</string>
</resources>