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

Bulk rewrite continues

This commit is contained in:
Steffo 2024-01-12 17:59:16 +01:00
parent 5befff9864
commit 01e744883f
Signed by: steffo
GPG key ID: 2A24051445686895
74 changed files with 1644 additions and 1158 deletions

View file

@ -36,7 +36,6 @@
android:theme="@style/Theme.TwoM">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
@ -46,13 +45,17 @@
android:theme="@style/Theme.TwoM" />
<activity
android:name=".activities.RoomActivity"
android:name=".activities.ViewRoomActivity"
android:theme="@style/Theme.TwoM" />
<activity
android:name=".activities.CreateRoomActivity"
android:theme="@style/Theme.TwoM" />
<activity
android:name=".activities.InviteUserActivity"
android:theme="@style/Theme.TwoM.BottomSheetDialog" />
</application>
</manifest>

View file

@ -0,0 +1,35 @@
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 androidx.compose.material3.Text
class InviteUserActivity : ComponentActivity() {
companion object {
const val USER_EXTRA = "user"
}
class Contract : ActivityResultContract<Unit, String?>() {
override fun createIntent(context: Context, input: Unit): Intent {
return Intent(context, CreateRoomActivity::class.java)
}
override fun parseResult(resultCode: Int, intent: Intent?): String? {
return when (resultCode) {
RESULT_OK -> intent!!.getStringExtra(USER_EXTRA)
else -> null
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent { Text("Garasauto Prime") }
}
}

View file

@ -9,12 +9,17 @@ import eu.steffo.twom.composables.login.LoginScaffold
class LoginActivity : ComponentActivity() {
class Contract : ActivityResultContract<Unit, Unit>() {
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 parseResult(resultCode: Int, intent: Intent?): Unit? {
return when (resultCode) {
RESULT_OK -> Unit
else -> null
}
}
}
override fun onStart() {

View file

@ -7,12 +7,10 @@ import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.net.toFile
import androidx.lifecycle.lifecycleScope
import eu.steffo.twom.composables.main.MainScaffold
import eu.steffo.twom.matrix.TwoMMatrix
import eu.steffo.twom.utils.TwoMGlobals
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
@ -21,38 +19,15 @@ import org.matrix.android.sdk.api.session.room.model.create.CreateRoomStateEvent
class MainActivity : ComponentActivity() {
private lateinit var loginLauncher: ActivityResultLauncher<Intent>
private lateinit var roomLauncher: ActivityResultLauncher<Intent>
private lateinit var createLauncher: ActivityResultLauncher<Intent>
private var session: Session? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
TwoMMatrix.ensureMatrix(applicationContext)
TwoMGlobals.ensureMatrix(applicationContext)
fetchLastSession()
openSession()
loginLauncher =
registerForActivityResult(
ActivityResultContracts.StartActivityForResult(),
this::onLogin
)
roomLauncher =
registerForActivityResult(
ActivityResultContracts.StartActivityForResult(),
this::onRoom
)
createLauncher =
registerForActivityResult(
ActivityResultContracts.StartActivityForResult(),
this::onCreate
)
resetContent()
}
@ -64,7 +39,7 @@ class MainActivity : ComponentActivity() {
private fun fetchLastSession() {
Log.d("Main", "Fetching the last successfully authenticated session...")
session = TwoMMatrix.matrix.authenticationService().getLastAuthenticatedSession()
session = TwoMGlobals.matrix.authenticationService().getLastAuthenticatedSession()
}
private fun openSession() {
@ -99,27 +74,6 @@ class MainActivity : ComponentActivity() {
}
}
private fun destroySession() {
}
private fun onClickRoom(roomId: String) {
Log.d("Main", "Clicked a room, launching room activity...")
val intent = Intent(applicationContext, RoomActivity::class.java)
intent.putExtra(RoomActivity.ROOM_ID_EXTRA, roomId)
roomLauncher.launch(intent)
}
private fun onRoom(result: ActivityResult) {
Log.d("Main", "Received result from room activity: $result")
}
private fun onClickCreate() {
Log.d("Main", "Clicked the New button, launching create activity...")
val intent = Intent(applicationContext, CreateRoomActivity::class.java)
createLauncher.launch(intent)
}
private fun onCreate(result: ActivityResult) {
Log.d("Main", "Received result from create activity: $result")
if (result.resultCode == RESULT_OK) {
@ -146,7 +100,7 @@ class MainActivity : ComponentActivity() {
createRoomParams.name = name
createRoomParams.topic = description
createRoomParams.preset = CreateRoomPreset.PRESET_PRIVATE_CHAT
createRoomParams.roomType = TwoMMatrix.ROOM_TYPE
createRoomParams.roomType = TwoMGlobals.ROOM_TYPE
createRoomParams.initialStates = mutableListOf(
CreateRoomStateEvent(
type = "m.room.power_levels",

View file

@ -7,18 +7,18 @@ 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 eu.steffo.twom.composables.viewroom.ViewRoomScaffold
import eu.steffo.twom.utils.TwoMGlobals
import org.matrix.android.sdk.api.session.Session
class RoomActivity : ComponentActivity() {
class ViewRoomActivity : ComponentActivity() {
companion object {
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)
val intent = Intent(context, ViewRoomActivity::class.java)
intent.putExtra(ROOM_ID_EXTRA, input)
return intent
}
@ -31,7 +31,7 @@ class RoomActivity : ComponentActivity() {
private fun fetchLastSession() {
Log.d("Main", "Fetching the last successfully authenticated session...")
// FIXME: If this is null, it means that something launched this while no session was authenticated...
session = TwoMMatrix.matrix.authenticationService().getLastAuthenticatedSession()!!
session = TwoMGlobals.matrix.authenticationService().getLastAuthenticatedSession()!!
}
override fun onCreate(savedInstanceState: Bundle?) {
@ -47,13 +47,9 @@ class RoomActivity : ComponentActivity() {
val roomId = intent.getStringExtra(ROOM_ID_EXTRA)
setContent {
RoomActivityScaffold(
ViewRoomScaffold(
session = session,
roomId = roomId!!, // FIXME: Again, this should be set. Should.
onBack = {
setResult(RESULT_CANCELED)
finish()
},
)
}
}

View file

@ -1,4 +1,4 @@
package eu.steffo.twom.matrix.avatar
package eu.steffo.twom.composables.avatar
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
@ -8,16 +8,13 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.tooling.preview.Preview
@Composable
@Preview(widthDp = 40, heightDp = 40)
fun AvatarFromDefault(
fun AvatarEmpty(
modifier: Modifier = Modifier,
fallbackText: String = "?",
contentDescription: String = "",
text: String? = null,
) {
Box(
modifier = modifier
@ -26,12 +23,9 @@ fun AvatarFromDefault(
) {
Text(
modifier = Modifier
.align(Alignment.Center)
.semantics {
this.contentDescription = ""
},
.align(Alignment.Center),
color = MaterialTheme.colorScheme.onTertiary,
text = fallbackText,
text = text ?: "?",
)
}
}
}

View file

@ -1,4 +1,4 @@
package eu.steffo.twom.matrix.avatar
package eu.steffo.twom.composables.avatar
import androidx.compose.foundation.Image
import androidx.compose.runtime.Composable
@ -6,21 +6,25 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.tooling.preview.Preview
@Composable
@Preview(widthDp = 40, heightDp = 40)
fun AvatarFromImageBitmap(
fun AvatarImage(
modifier: Modifier = Modifier,
bitmap: ImageBitmap? = null,
fallbackText: String = "?",
fallbackText: String? = null,
contentDescription: String = "",
) {
if (bitmap == null) {
AvatarFromDefault(
modifier = modifier,
fallbackText = fallbackText,
contentDescription = contentDescription,
AvatarEmpty(
modifier = modifier
.semantics {
this.contentDescription = contentDescription
},
text = fallbackText,
)
} else {
Image(

View file

@ -15,7 +15,6 @@ 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.matrix.avatar.AvatarFromImageBitmap
import eu.steffo.twom.utils.BitmapUtilities
@Composable
@ -49,7 +48,7 @@ fun AvatarPicker(
launcher.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly))
}
) {
AvatarFromImageBitmap(
AvatarImage(
bitmap = selection?.asImageBitmap(),
fallbackText = fallbackText,
)

View file

@ -1,4 +1,4 @@
package eu.steffo.twom.matrix.avatar
package eu.steffo.twom.composables.avatar
import android.graphics.Bitmap
import android.graphics.BitmapFactory
@ -12,16 +12,15 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.tooling.preview.Preview
import eu.steffo.twom.matrix.LocalSession
import org.matrix.android.sdk.api.failure.Failure
import eu.steffo.twom.composables.matrix.LocalSession
import java.io.File
@Composable
@Preview(widthDp = 40, heightDp = 40)
fun AvatarFromURL(
fun AvatarURL(
modifier: Modifier = Modifier,
url: String? = "",
fallbackText: String = "?",
fallbackText: String? = null,
contentDescription: String = "",
) {
val session = LocalSession.current
@ -29,22 +28,22 @@ fun AvatarFromURL(
LaunchedEffect(session, url) GetAvatar@{
if (session == null) {
Log.d("Avatar", "Not doing anything, session is null.")
Log.d("AvatarURL", "Not doing anything, session is null.")
bitmap = null
return@GetAvatar
}
if (url == null) {
Log.d("Avatar", "URL is null, not downloading anything.")
Log.d("AvatarURL", "URL is null, not downloading anything.")
bitmap = null
return@GetAvatar
}
if (url.isEmpty()) {
Log.d("Avatar", "URL is a zero-length string, not downloading anything.")
Log.d("AvatarURL", "URL is a zero-length string, not downloading anything.")
bitmap = null
return@GetAvatar
}
Log.d("Avatar", "Downloading avatar at: $url")
Log.d("AvatarURL", "Downloading avatar at: $url")
lateinit var avatarFile: File
try {
avatarFile = session.fileService().downloadFile(
@ -53,27 +52,20 @@ fun AvatarFromURL(
mimeType = null,
elementToDecrypt = null,
)
} catch (f: Failure.OtherServerError) {
Log.e("Avatar", "Unable to download avatar at: $url", f)
} catch (e: Throwable) {
Log.e("AvatarURL", "Unable to download avatar at: $url", e)
return@GetAvatar
}
// TODO: Should I check the MIME type? And the size of the image?
Log.d("Avatar", "File for $url is: $avatarFile")
Log.d("AvatarURL", "File for $url is: $avatarFile")
bitmap = BitmapFactory.decodeFile(avatarFile.absolutePath)
}
if (bitmap == null) {
AvatarFromDefault(
modifier = modifier,
fallbackText = fallbackText,
contentDescription = contentDescription
)
} else {
AvatarFromImageBitmap(
modifier = modifier,
bitmap = bitmap!!.asImageBitmap(),
contentDescription = contentDescription,
)
}
AvatarImage(
modifier = modifier,
bitmap = bitmap?.asImageBitmap(),
fallbackText = fallbackText,
contentDescription = contentDescription,
)
}

View file

@ -0,0 +1,23 @@
package eu.steffo.twom.composables.avatar
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import org.matrix.android.sdk.api.session.user.model.User
import org.matrix.android.sdk.api.util.toMatrixItem
@Composable
@Preview(widthDp = 40, heightDp = 40)
fun AvatarUser(
modifier: Modifier = Modifier,
user: User? = null,
fallbackText: String? = null,
contentDescription: String = "",
) {
AvatarURL(
modifier = modifier,
url = user?.avatarUrl,
fallbackText = user?.toMatrixItem()?.firstLetterOfDisplayName(),
contentDescription = contentDescription,
)
}

View file

@ -1,4 +1,4 @@
package eu.steffo.twom.matrix.avatar
package eu.steffo.twom.composables.avatar
import android.util.Log
import androidx.compose.runtime.Composable
@ -9,11 +9,11 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import eu.steffo.twom.matrix.LocalSession
import eu.steffo.twom.composables.matrix.LocalSession
@Composable
@Preview(widthDp = 40, heightDp = 40)
fun AvatarFromUserId(
fun AvatarUserId(
modifier: Modifier = Modifier,
userId: String = "",
fallbackText: String = "?",
@ -24,30 +24,24 @@ fun AvatarFromUserId(
LaunchedEffect(session, userId) GetAvatarUrl@{
if (session == null) {
Log.d("UserAvatar", "Not doing anything, session is null.")
Log.d("AvatarUser", "Not doing anything, session is null.")
return@GetAvatarUrl
}
if (userId.isEmpty()) {
Log.d("UserAvatar", "Not doing anything, userId is empty.")
Log.d("AvatarUser", "Not doing anything, userId is empty.")
return@GetAvatarUrl
}
Log.d("UserAvatar", "Retrieving avatar url for: $userId...")
Log.d("AvatarUser", "Retrieving avatar url for: $userId...")
avatarUrl = session.profileService().getAvatarUrl(userId).getOrNull()
Log.d("UserAvatar", "Retrieved avatar url for $userId: $avatarUrl")
Log.d("AvatarUser", "Retrieved avatar url for $userId: $avatarUrl")
}
if (avatarUrl == null) {
AvatarFromDefault(
modifier = modifier,
fallbackText = fallbackText,
contentDescription = contentDescription,
)
} else {
AvatarFromURL(
modifier = modifier,
url = avatarUrl!!,
fallbackText = fallbackText,
contentDescription = contentDescription,
)
}
}
AvatarURL(
modifier = modifier,
url = avatarUrl,
fallbackText = fallbackText,
contentDescription = contentDescription,
)
}

View file

@ -25,7 +25,7 @@ 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.composables.theme.basePadding
import eu.steffo.twom.utils.BitmapUtilities
@Composable
@ -39,7 +39,7 @@ fun CreateRoomForm(
var avatarUri by rememberSaveable { mutableStateOf<Uri?>(null) }
Column(modifier) {
Row(TwoMPadding.base) {
Row(Modifier.basePadding()) {
val avatarContentDescription = stringResource(R.string.create_avatar_label)
AvatarPicker(
modifier = Modifier
@ -67,7 +67,7 @@ fun CreateRoomForm(
)
}
Row(TwoMPadding.base) {
Row(Modifier.basePadding()) {
TextField(
modifier = Modifier
.height(180.dp)
@ -80,7 +80,7 @@ fun CreateRoomForm(
)
}
Row(TwoMPadding.base) {
Row(Modifier.basePadding()) {
Button(
modifier = Modifier
.fillMaxWidth(),

View file

@ -11,7 +11,7 @@ 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
import eu.steffo.twom.composables.theme.TwoMTheme
@Composable
@Preview

View file

@ -0,0 +1,66 @@
package eu.steffo.twom.composables.errorhandling
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import eu.steffo.twom.R
@Composable
@Preview
fun ErrorIconButton(
message: String = "Placeholder",
) {
var expanded by rememberSaveable { mutableStateOf(false) }
IconButton(
onClick = { expanded = true },
) {
Icon(
imageVector = Icons.Filled.Warning,
contentDescription = stringResource(R.string.error),
tint = MaterialTheme.colorScheme.error,
)
}
if (expanded) {
AlertDialog(
onDismissRequest = { expanded = false },
icon = {
Icon(
imageVector = Icons.Filled.Warning,
contentDescription = stringResource(R.string.error),
)
},
title = {
Text(stringResource(R.string.error))
},
text = {
Text(message)
},
confirmButton = {
TextButton(
onClick = { expanded = false },
) {
Text(stringResource(R.string.close))
}
},
containerColor = MaterialTheme.colorScheme.errorContainer,
iconContentColor = MaterialTheme.colorScheme.onErrorContainer,
titleContentColor = MaterialTheme.colorScheme.onErrorContainer,
textContentColor = MaterialTheme.colorScheme.onErrorContainer,
)
}
}

View file

@ -0,0 +1,56 @@
package eu.steffo.twom.composables.errorhandling
import androidx.annotation.StringRes
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
class LocalizableError {
@StringRes
var stringResourceId: Int? = null
private set
var throwable: Throwable? = null
private set
fun set(stringResourceId: Int) {
this.stringResourceId = stringResourceId
this.throwable = null
}
fun set(stringResourceId: Int, throwable: Throwable) {
this.stringResourceId = stringResourceId
this.throwable = throwable
}
fun clear() {
this.stringResourceId = null
this.throwable = null
}
fun occurred(): Boolean {
return stringResourceId != null
}
@Composable
fun renderString(): String? {
val stringResourceId = this.stringResourceId
val throwable = this.throwable
return if (stringResourceId == null) {
null
} else if (throwable == null) {
stringResource(stringResourceId)
} else {
stringResource(stringResourceId, throwable.toString())
}
}
@Composable
fun Show(contents: @Composable (rendered: String) -> Unit) {
val rendered = renderString()
if (rendered != null) {
contents(rendered)
}
}
}

View file

@ -0,0 +1,47 @@
package eu.steffo.twom.composables.inviteuser
import androidx.compose.foundation.layout.Row
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import eu.steffo.twom.R
import eu.steffo.twom.composables.theme.basePadding
@Composable
fun InviteUserContent() {
Row(Modifier.basePadding()) {
Text(
text = stringResource(R.string.room_invite_title),
style = MaterialTheme.typography.labelLarge,
)
}
Row(Modifier.basePadding()) {
InviteUserForm(
/*
onConfirm = {
scope.launch SendInvite@{
isSendingInvite = true
errorInvite = null
Log.d("Room", "Sending invite to `$it`...")
try {
room.membershipService().invite(it)
} catch (error: Throwable) {
Log.e("Room", "Failed to send invite to `$it`: $error")
errorInvite = error
isSendingInvite = false
return@SendInvite
}
Log.d("Room", "Successfully sent invite to `$it`!")
isSendingInvite = false
}
}
*/
)
}
}

View file

@ -0,0 +1,54 @@
package eu.steffo.twom.composables.inviteuser
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import eu.steffo.twom.R
@Composable
@Preview
fun InviteUserForm(
onConfirm: (userId: String) -> Unit = {},
) {
var value by rememberSaveable { mutableStateOf("") }
OutlinedTextField(
modifier = Modifier
.fillMaxWidth(),
value = value,
onValueChange = { value = it },
singleLine = true,
shape = MaterialTheme.shapes.small,
placeholder = {
Text(
text = stringResource(R.string.room_invite_username_placeholder)
)
},
)
Button(
modifier = Modifier
.padding(top = 4.dp)
.fillMaxWidth(),
onClick = { onConfirm(value) },
shape = MaterialTheme.shapes.small,
// FIXME: Maybe I should validate usernames with a regex
enabled = (value.contains("@") && value.contains(":")),
) {
Text(
text = stringResource(R.string.room_invite_button_label)
)
}
}

View file

@ -24,8 +24,8 @@ 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 eu.steffo.twom.composables.theme.basePadding
import eu.steffo.twom.utils.TwoMGlobals
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
import org.matrix.android.sdk.api.auth.data.LoginFlowResult
@ -66,7 +66,7 @@ fun LoginForm(
Log.d("Login", "Getting authentication service...")
loginStep = LoginStep.SERVICE
val auth = TwoMMatrix.matrix.authenticationService()
val auth = TwoMGlobals.matrix.authenticationService()
Log.d("Login", "Resetting authentication service...")
auth.reset()
@ -174,10 +174,10 @@ fun LoginForm(
progress = loginStep.step.toFloat() / LoginStep.DONE.step.toFloat(),
color = if (error.occurred()) MaterialTheme.colorScheme.error else ProgressIndicatorDefaults.linearColor
)
Row(TwoMPadding.base) {
Row(Modifier.basePadding()) {
Text(LocalContext.current.getString(R.string.login_text))
}
Row(TwoMPadding.base) {
Row(Modifier.basePadding()) {
TextField(
modifier = Modifier.fillMaxWidth(),
singleLine = true,
@ -194,7 +194,7 @@ fun LoginForm(
},
)
}
Row(TwoMPadding.base) {
Row(Modifier.basePadding()) {
PasswordField(
modifier = Modifier.fillMaxWidth(),
singleLine = true,
@ -211,7 +211,7 @@ fun LoginForm(
},
)
}
Row(TwoMPadding.base) {
Row(Modifier.basePadding()) {
Button(
modifier = Modifier.fillMaxWidth(),
enabled = (username != "" && (loginStep == LoginStep.NONE || error.occurred())),
@ -223,7 +223,7 @@ fun LoginForm(
}
}
error.Show {
Row(TwoMPadding.base) {
Row(Modifier.basePadding()) {
ErrorText(it)
}
}

View file

@ -9,7 +9,7 @@ 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 eu.steffo.twom.composables.theme.TwoMTheme
import org.matrix.android.sdk.api.session.Session

View file

@ -21,8 +21,8 @@ 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
import eu.steffo.twom.composables.avatar.AvatarUserId
import eu.steffo.twom.composables.matrix.LocalSession
@Composable
@Preview(showBackground = true)
@ -35,7 +35,11 @@ fun AccountIconButton(
var expanded by remember { mutableStateOf(false) }
val loginLauncher =
rememberLauncherForActivityResult(LoginActivity.Contract()) { processLogin() }
rememberLauncherForActivityResult(LoginActivity.Contract()) {
if (it != null) {
processLogin()
}
}
Box(modifier) {
IconButton(
@ -47,7 +51,7 @@ fun AccountIconButton(
contentDescription = LocalContext.current.getString(R.string.main_account_label),
)
} else {
AvatarFromUserId(
AvatarUserId(
userId = session.myUserId,
contentDescription = LocalContext.current.getString(R.string.main_account_label),
)
@ -60,7 +64,7 @@ fun AccountIconButton(
if (session == null) {
DropdownMenuItem(
text = {
Text(stringResource(id = R.string.main_account_login_text))
Text(stringResource(R.string.main_account_login_text))
},
onClick = {
expanded = false
@ -70,7 +74,7 @@ fun AccountIconButton(
} else {
DropdownMenuItem(
text = {
Text(stringResource(id = R.string.main_account_logout_text))
Text(stringResource(R.string.main_account_logout_text))
},
onClick = {
expanded = false
@ -80,5 +84,4 @@ fun AccountIconButton(
}
}
}
}

View file

@ -1,5 +1,7 @@
package eu.steffo.twom.composables.main
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.launch
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material3.ExtendedFloatingActionButton
@ -10,16 +12,24 @@ 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.activities.CreateRoomActivity
@Composable
@Preview
fun CreateRoomFAB(
modifier: Modifier = Modifier,
onClick: () -> Unit = {},
onCreateParamsSelected: (name: String, description: String, avatarUri: String?) -> Unit = { _, _, _ -> },
) {
val launcher =
rememberLauncherForActivityResult(CreateRoomActivity.Contract()) {
if (it != null) {
onCreateParamsSelected(it.name, it.description, it.avatarUri)
}
}
ExtendedFloatingActionButton(
modifier = modifier,
onClick = onClick,
onClick = { launcher.launch() },
icon = {
Icon(
Icons.Filled.Add,

View file

@ -8,11 +8,10 @@ import androidx.compose.runtime.livedata.observeAsState
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 eu.steffo.twom.composables.errorhandling.ErrorText
import eu.steffo.twom.composables.matrix.LocalSession
import eu.steffo.twom.composables.theme.basePadding
import eu.steffo.twom.utils.TwoMGlobals
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
@ -29,19 +28,19 @@ fun MainContentLoggedIn(
val roomSummaries by session.roomService().getRoomSummariesLive(
roomSummaryQueryParams {
this.memberships = listOf(Membership.JOIN)
this.includeType = listOf(TwoMMatrix.ROOM_TYPE)
this.includeType = listOf(TwoMGlobals.ROOM_TYPE)
}
).observeAsState()
Column(modifier) {
if (roomSummaries == null) {
Text(
modifier = TwoMPadding.base,
modifier = Modifier.basePadding(),
text = stringResource(R.string.loading)
)
} else if (roomSummaries!!.isEmpty()) {
Text(
modifier = TwoMPadding.base,
modifier = Modifier.basePadding(),
text = stringResource(R.string.main_roomlist_empty_text)
)
} else {

View file

@ -8,7 +8,7 @@ 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.theme.TwoMPadding
import eu.steffo.twom.composables.theme.basePadding
@Composable
@Preview(showBackground = true)
@ -17,10 +17,10 @@ fun MainContentNotLoggedIn(
onClickLogin: () -> Unit = {},
) {
Column(modifier) {
Row(TwoMPadding.base) {
Row(Modifier.basePadding()) {
Text(LocalContext.current.getString(R.string.main_notloggedin_text_1))
}
Row(TwoMPadding.base) {
Row(Modifier.basePadding()) {
Text(LocalContext.current.getString(R.string.main_notloggedin_text_2))
}
}

View file

@ -6,8 +6,8 @@ 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.matrix.LocalSession
import eu.steffo.twom.theme.TwoMTheme
import eu.steffo.twom.composables.matrix.LocalSession
import eu.steffo.twom.composables.theme.TwoMTheme
import org.matrix.android.sdk.api.session.Session
@Composable
@ -15,6 +15,7 @@ import org.matrix.android.sdk.api.session.Session
fun MainScaffold(
processLogin: () -> Unit = {},
processLogout: () -> Unit = {},
processCreate: (name: String, description: String, avatarUri: String?) -> Unit = { _, _, _ -> },
session: Session? = null,
) {
TwoMTheme {
@ -27,7 +28,9 @@ fun MainScaffold(
)
},
floatingActionButton = {
CreateRoomFAB()
CreateRoomFAB(
onCreateParamsSelected = processCreate,
)
},
content = {
if (session == null) {

View file

@ -1,4 +1,4 @@
package eu.steffo.twom.main
package eu.steffo.twom.composables.main
import android.util.Log
import androidx.activity.compose.rememberLauncherForActivityResult
@ -23,11 +23,11 @@ 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.activities.ViewRoomActivity
import eu.steffo.twom.composables.avatar.AvatarURL
import eu.steffo.twom.composables.errorhandling.ErrorText
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 eu.steffo.twom.composables.matrix.LocalSession
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.session.room.model.RoomSummary
@ -50,11 +50,11 @@ fun RoomListItem(
var expanded by rememberSaveable { mutableStateOf(false) }
val error by remember { mutableStateOf(LocalizableError()) }
val roomActivityLauncher = rememberLauncherForActivityResult(RoomActivity.Contract()) {}
val viewRoomActivityLauncher = rememberLauncherForActivityResult(ViewRoomActivity.Contract()) {}
fun openRoom() {
Log.i("Main", "Opening room `$roomId`...")
roomActivityLauncher.launch(roomId)
viewRoomActivityLauncher.launch(roomId)
}
suspend fun leaveRoom() {
@ -83,7 +83,7 @@ fun RoomListItem(
.size(40.dp)
.clip(MaterialTheme.shapes.medium)
) {
AvatarFromURL(
AvatarURL(
// FIXME: URL can appearently be set before the image is available on the homeserver
url = roomSummary.avatarUrl,
)

View file

@ -1,4 +1,4 @@
package eu.steffo.twom.matrix
package eu.steffo.twom.composables.matrix
import androidx.compose.runtime.staticCompositionLocalOf
import org.matrix.android.sdk.api.session.Session

View file

@ -0,0 +1,34 @@
package eu.steffo.twom.composables.navigation
import android.app.Activity
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import eu.steffo.twom.R
@Composable
fun BackIconButton(
modifier: Modifier = Modifier,
) {
val context = LocalContext.current
val activity = context as Activity
fun cancelActivity() {
activity.setResult(Activity.RESULT_CANCELED)
activity.finish()
}
IconButton(
modifier = modifier,
onClick = { cancelActivity() }
) {
Icon(
imageVector = Icons.Filled.ArrowBack,
contentDescription = LocalContext.current.getString(R.string.back)
)
}
}

View file

@ -0,0 +1,15 @@
package eu.steffo.twom.composables.theme
import androidx.compose.ui.graphics.Color
object LaterColorRole : StaticColorRole {
override val lightColor = Color(0xFF00658B)
override val lightOnColor = Color(0xFFFFFFFF)
override val lightContainerColor = Color(0xFFC4E7FF)
override val lightOnContainerColor = Color(0xFF001E2C)
override val darkColor = Color(0xFF7DD0FF)
override val darkOnColor = Color(0xFF00344A)
override val darkContainerColor = Color(0xFF004C69)
override val darkOnContainerColor = Color(0xFFC4E7FF)
}

View file

@ -0,0 +1,15 @@
package eu.steffo.twom.composables.theme
import androidx.compose.ui.graphics.Color
object MaybeColorRole : StaticColorRole {
override val lightColor = Color(0xFF765B00)
override val lightOnColor = Color(0xFFFFFFFF)
override val lightContainerColor = Color(0xFFFFDF94)
override val lightOnContainerColor = Color(0xFF241A00)
override val darkColor = Color(0xFFEDC148)
override val darkOnColor = Color(0xFF3E2E00)
override val darkContainerColor = Color(0xFF594400)
override val darkOnContainerColor = Color(0xFFFFDF94)
}

View file

@ -0,0 +1,15 @@
package eu.steffo.twom.composables.theme
import androidx.compose.ui.graphics.Color
object NowayColorRole : StaticColorRole {
override val lightColor = Color(0xFFAB3520)
override val lightOnColor = Color(0xFFFFFFFF)
override val lightContainerColor = Color(0xFFFFDAD3)
override val lightOnContainerColor = Color(0xFF3F0400)
override val darkColor = Color(0xFFFFB4A5)
override val darkOnColor = Color(0xFF650A00)
override val darkContainerColor = Color(0xFF891D0A)
override val darkOnContainerColor = Color(0xFFFFDAD3)
}

View file

@ -0,0 +1,15 @@
package eu.steffo.twom.composables.theme
import androidx.compose.ui.graphics.Color
object NullishColorRole : StaticColorRole {
override val lightColor = Color(0xFF666666)
override val lightOnColor = Color(0xFFFFFFFF)
override val lightContainerColor = Color(0xFFE6E6E6)
override val lightOnContainerColor = Color(0xFF222222)
override val darkColor = Color(0xFFDDDDDD)
override val darkOnColor = Color(0xFF333333)
override val darkContainerColor = Color(0xFF555555)
override val darkOnContainerColor = Color(0xFFFFFFFF)
}

View file

@ -0,0 +1,69 @@
package eu.steffo.twom.composables.theme
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import com.google.android.material.color.MaterialColors
interface StaticColorRole {
val lightColor: Color
val lightOnColor: Color
val lightContainerColor: Color
val lightOnContainerColor: Color
val darkColor: Color
val darkOnColor: Color
val darkContainerColor: Color
val darkOnContainerColor: Color
@Composable
fun harmonize(color: Color): Color {
val ctx = LocalContext.current
val colorArgb = color.toArgb()
val colorArgbHarmonized = MaterialColors.harmonizeWithPrimary(ctx, colorArgb)
return Color(colorArgbHarmonized)
}
@Composable
fun color(): Color {
return harmonize(
when (isSystemInDarkTheme()) {
false -> lightColor
true -> darkColor
}
)
}
@Composable
fun onColor(): Color {
return harmonize(
when (isSystemInDarkTheme()) {
false -> lightOnColor
true -> darkOnColor
}
)
}
@Composable
fun containerColor(): Color {
return harmonize(
when (isSystemInDarkTheme()) {
false -> lightContainerColor
true -> darkContainerColor
}
)
}
@Composable
fun onContainerColor(): Color {
return harmonize(
when (isSystemInDarkTheme()) {
false -> lightOnContainerColor
true -> darkOnContainerColor
}
)
}
}

View file

@ -0,0 +1,15 @@
package eu.steffo.twom.composables.theme
import androidx.compose.ui.graphics.Color
object SureColorRole : StaticColorRole {
override val lightColor = Color(0xFF006E2C)
override val lightOnColor = Color(0xFFFFFFFF)
override val lightContainerColor = Color(0xFF7FFC95)
override val lightOnContainerColor = Color(0xFF002108)
override val darkColor = Color(0xFF62DF7C)
override val darkOnColor = Color(0xFF003913)
override val darkContainerColor = Color(0xFF00531F)
override val darkOnContainerColor = Color(0xFF7FFC95)
}

View file

@ -0,0 +1,14 @@
package eu.steffo.twom.composables.theme
import androidx.compose.foundation.layout.padding
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
fun Modifier.basePadding(): Modifier {
return this.padding(all = 10.dp)
}
fun Modifier.chipPadding(): Modifier {
return this.padding(start = 2.5.dp, end = 2.5.dp)
}

View file

@ -1,4 +1,4 @@
package eu.steffo.twom.theme
package eu.steffo.twom.composables.theme
import android.app.Activity
import androidx.compose.foundation.isSystemInDarkTheme
@ -18,6 +18,7 @@ import androidx.core.view.WindowCompat
fun TwoMTheme(
content: @Composable () -> Unit
) {
val view = LocalView.current
val context = LocalContext.current
val darkTheme = isSystemInDarkTheme()
@ -27,21 +28,21 @@ fun TwoMTheme(
}
val typography = Typography()
MaterialTheme(
colorScheme = colorScheme,
typography = typography,
content = content
)
val view = LocalView.current
if (!view.isInEditMode) {
SideEffect {
val window = (view.context as Activity).window
val window = (context as Activity).window
window.statusBarColor = colorScheme.surface.toArgb()
window.navigationBarColor = colorScheme.surface.toArgb()
val insets = WindowCompat.getInsetsController(window, view)
insets.isAppearanceLightStatusBars = !darkTheme
insets.isAppearanceLightNavigationBars = !darkTheme
}
}
MaterialTheme(
colorScheme = colorScheme,
typography = typography,
content = content
)
}

View file

@ -0,0 +1,43 @@
package eu.steffo.twom.composables.viewroom
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.launch
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
import eu.steffo.twom.activities.InviteUserActivity
@Composable
@Preview
fun InviteFAB(
modifier: Modifier = Modifier,
onUserSelected: (userId: String) -> Unit = {},
) {
val launcher =
rememberLauncherForActivityResult(InviteUserActivity.Contract()) {
if (it != null) {
onUserSelected(it)
}
}
ExtendedFloatingActionButton(
modifier = modifier,
onClick = { launcher.launch() },
icon = {
Icon(
Icons.Filled.Add,
contentDescription = null
)
},
text = {
Text(stringResource(R.string.room_invite_button_label))
}
)
}

View file

@ -1,4 +1,4 @@
package eu.steffo.twom.room
package eu.steffo.twom.composables.viewroom
import androidx.compose.runtime.staticCompositionLocalOf
import org.matrix.android.sdk.api.session.room.Room

View file

@ -1,4 +1,4 @@
package eu.steffo.twom.room
package eu.steffo.twom.composables.viewroom
import androidx.compose.runtime.staticCompositionLocalOf
import org.matrix.android.sdk.api.session.room.model.RoomSummary

View file

@ -0,0 +1,123 @@
package eu.steffo.twom.composables.viewroom
import android.util.Log
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
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.res.stringResource
import androidx.compose.ui.unit.dp
import eu.steffo.twom.R
import eu.steffo.twom.composables.avatar.AvatarUser
import eu.steffo.twom.composables.errorhandling.ErrorText
import eu.steffo.twom.composables.matrix.LocalSession
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
import org.matrix.android.sdk.api.session.user.model.User
import kotlin.jvm.optionals.getOrNull
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun MemberListItem(
member: RoomMemberSummary,
modifier: Modifier = Modifier,
) {
val session = LocalSession.current
if (session == null) {
ErrorText(stringResource(R.string.error_session_missing))
return
}
val roomRequest = LocalRoom.current
if (roomRequest == null) {
ErrorText(stringResource(R.string.room_error_room_missing))
return
}
val room = roomRequest.getOrNull()
if (room == null) {
ErrorText(stringResource(R.string.room_error_room_notfound))
return
}
// TODO: Is this necessary?
var user by remember { mutableStateOf<User?>(null) }
LaunchedEffect(session, member.userId) {
val memberId = member.userId
Log.d("UserListItem", "Resolving user: $memberId")
user = session.userService().resolveUser(memberId)
Log.d("UserListItem", "Resolved user: $memberId")
}
val rsvp = observeRSVP(room = room, member = member)
var expanded by rememberSaveable { mutableStateOf(false) }
ListItem(
modifier = modifier.combinedClickable(
onClick = {},
onLongClick = { expanded = true },
),
headlineContent = {
Text(
text = user?.displayName ?: stringResource(R.string.user_unresolved_name),
)
},
leadingContent = {
Box(
Modifier
.padding(end = 10.dp)
.size(40.dp)
.clip(MaterialTheme.shapes.extraLarge)
) {
AvatarUser(
user = user,
)
}
},
trailingContent = {
Icon(
imageVector = rsvp.answer.icon,
contentDescription = rsvp.answer.toResponse(),
tint = rsvp.answer.staticColorRole.color(),
)
},
supportingContent = {
if (rsvp.comment != "") {
Text(
text = rsvp.comment,
color = rsvp.answer.staticColorRole.color(),
)
}
},
)
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
DropdownMenuItem(
text = {
Text(stringResource(R.string.room_uninvite_label))
},
onClick = { expanded = false }
)
}
}

View file

@ -1,4 +1,4 @@
package eu.steffo.twom.room
package eu.steffo.twom.composables.viewroom
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilterChip
@ -8,47 +8,45 @@ import androidx.compose.material3.MaterialTheme
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 androidx.compose.ui.unit.dp
import eu.steffo.twom.utils.RSVPAnswer
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun RSVPAnswerFilterChip(
@Preview
fun RSVPChip(
modifier: Modifier = Modifier,
representing: RSVPAnswer,
selected: Boolean = false,
onClick: () -> Unit = {},
representedAnswer: RSVPAnswer = RSVPAnswer.UNKNOWN,
) {
val icon = representing.toIcon()
val colorRole = representing.toStaticColorRole()
val labelResourceId = representing.toLabelResourceId()
FilterChip(
modifier = modifier,
selected = selected,
onClick = onClick,
leadingIcon = {
Icon(
imageVector = icon,
imageVector = representedAnswer.icon,
contentDescription = null,
)
},
label = {
Text(
text = stringResource(labelResourceId),
text = representedAnswer.toLabel() ?: "[missing label]",
style = MaterialTheme.typography.bodyLarge,
)
},
colors = FilterChipDefaults.filterChipColors(
iconColor = colorRole.value,
labelColor = colorRole.value,
selectedContainerColor = colorRole.valueContainer,
selectedLeadingIconColor = colorRole.onValueContainer,
selectedLabelColor = colorRole.onValueContainer,
iconColor = representedAnswer.staticColorRole.color(),
labelColor = representedAnswer.staticColorRole.color(),
selectedContainerColor = representedAnswer.staticColorRole.containerColor(),
selectedLeadingIconColor = representedAnswer.staticColorRole.onContainerColor(),
selectedLabelColor = representedAnswer.staticColorRole.onContainerColor(),
),
border = FilterChipDefaults.filterChipBorder(
borderColor = MaterialTheme.colorScheme.surfaceVariant,
selectedBorderColor = colorRole.onValueContainer,
selectedBorderColor = representedAnswer.staticColorRole.onContainerColor(),
borderWidth = 1.dp,
selectedBorderWidth = 1.dp,
)

View file

@ -1,4 +1,4 @@
package eu.steffo.twom.room
package eu.steffo.twom.composables.viewroom
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Box
@ -9,11 +9,12 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import eu.steffo.twom.theme.TwoMPadding
import eu.steffo.twom.composables.theme.chipPadding
import eu.steffo.twom.utils.RSVPAnswer
@Composable
@Preview
fun RSVPAnswerSelectRow(
fun RSVPChipRow(
modifier: Modifier = Modifier,
value: RSVPAnswer = RSVPAnswer.UNKNOWN,
onChange: (answer: RSVPAnswer) -> Unit = {},
@ -37,27 +38,27 @@ fun RSVPAnswerSelectRow(
modifier = Modifier
.padding(start = 8.dp, end = 8.dp)
) {
RSVPAnswerFilterChip(
modifier = TwoMPadding.chips,
representing = RSVPAnswer.SURE,
RSVPChip(
modifier = Modifier.chipPadding(),
representedAnswer = RSVPAnswer.SURE,
selected = (value == RSVPAnswer.SURE),
onClick = toggleSwitch(RSVPAnswer.SURE)
)
RSVPAnswerFilterChip(
modifier = TwoMPadding.chips,
representing = RSVPAnswer.LATER,
RSVPChip(
modifier = Modifier.chipPadding(),
representedAnswer = RSVPAnswer.LATER,
selected = (value == RSVPAnswer.LATER),
onClick = toggleSwitch(RSVPAnswer.LATER)
)
RSVPAnswerFilterChip(
modifier = TwoMPadding.chips,
representing = RSVPAnswer.MAYBE,
RSVPChip(
modifier = Modifier.chipPadding(),
representedAnswer = RSVPAnswer.MAYBE,
selected = (value == RSVPAnswer.MAYBE),
onClick = toggleSwitch(RSVPAnswer.MAYBE)
)
RSVPAnswerFilterChip(
modifier = TwoMPadding.chips,
representing = RSVPAnswer.NOWAY,
RSVPChip(
modifier = Modifier.chipPadding(),
representedAnswer = RSVPAnswer.NOWAY,
selected = (value == RSVPAnswer.NOWAY),
onClick = toggleSwitch(RSVPAnswer.NOWAY)
)

View file

@ -0,0 +1,40 @@
package eu.steffo.twom.composables.viewroom
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.OutlinedTextFieldDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import eu.steffo.twom.utils.RSVPAnswer
@Composable
@Preview
fun RSVPCommentField(
modifier: Modifier = Modifier,
value: String = "",
onChange: (value: String) -> Unit = {},
currentAnswer: RSVPAnswer = RSVPAnswer.UNKNOWN,
) {
OutlinedTextField(
modifier = modifier,
value = value,
onValueChange = onChange,
singleLine = true,
shape = MaterialTheme.shapes.small,
placeholder = {
Text(currentAnswer.toCommentPlaceholder())
},
colors = OutlinedTextFieldDefaults.colors(
focusedContainerColor = currentAnswer.staticColorRole.containerColor(),
unfocusedContainerColor = currentAnswer.staticColorRole.containerColor(),
focusedTextColor = currentAnswer.staticColorRole.onContainerColor(),
unfocusedTextColor = currentAnswer.staticColorRole.onContainerColor(),
focusedBorderColor = currentAnswer.staticColorRole.onContainerColor(),
unfocusedBorderColor = currentAnswer.staticColorRole.onContainerColor()
.copy(alpha = 0.3f),
cursorColor = currentAnswer.staticColorRole.onContainerColor(),
)
)
}

View file

@ -0,0 +1,46 @@
package eu.steffo.twom.composables.viewroom
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import eu.steffo.twom.utils.RSVP
import eu.steffo.twom.utils.RSVPAnswer
@Composable
fun RSVPForm(
published: RSVP,
onRequestPublish: (newAnswer: RSVPAnswer, newComment: String) -> Unit = { _, _ -> },
isPublishRunning: Boolean = false,
) {
var currentAnswer by rememberSaveable { mutableStateOf(published.answer) }
var currentComment by rememberSaveable { mutableStateOf(published.comment) }
val hasChanged = (currentAnswer != published.answer || currentComment != published.comment)
RSVPChipRow(
value = currentAnswer,
onChange = { currentAnswer = it },
)
RSVPCommentField(
modifier = Modifier
.padding(start = 10.dp, end = 10.dp)
.fillMaxWidth(),
value = currentComment,
onChange = { currentComment = it },
currentAnswer = currentAnswer,
)
RSVPUpdateButton(
modifier = Modifier
.padding(start = 10.dp, end = 10.dp, top = 4.dp, bottom = 4.dp)
.fillMaxWidth(),
onClick = { onRequestPublish(currentAnswer, currentComment) },
enabled = hasChanged && !isPublishRunning,
currentAnswer = currentAnswer,
)
}

View file

@ -1,4 +1,4 @@
package eu.steffo.twom.room
package eu.steffo.twom.composables.viewroom
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
@ -9,6 +9,7 @@ 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.utils.RSVPAnswer
@Composable
@Preview
@ -16,22 +17,20 @@ fun RSVPUpdateButton(
modifier: Modifier = Modifier,
enabled: Boolean = true,
onClick: () -> Unit = {},
rsvpAnswer: RSVPAnswer = RSVPAnswer.UNKNOWN,
currentAnswer: RSVPAnswer = RSVPAnswer.UNKNOWN,
) {
val colorRole = rsvpAnswer.toStaticColorRole()
Button(
modifier = modifier,
enabled = enabled,
onClick = onClick,
shape = MaterialTheme.shapes.small,
colors = ButtonDefaults.buttonColors(
containerColor = colorRole.value,
contentColor = colorRole.onValue,
containerColor = currentAnswer.staticColorRole.color(),
contentColor = currentAnswer.staticColorRole.onColor(),
)
) {
Text(
text = stringResource(R.string.room_update_label)
text = stringResource(R.string.room_rsvp_update_label)
)
}
}

View file

@ -0,0 +1,64 @@
package eu.steffo.twom.composables.viewroom
import androidx.compose.foundation.layout.Box
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.IconButton
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.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import eu.steffo.twom.R
import eu.steffo.twom.composables.avatar.AvatarURL
@Composable
fun RoomIconButton(
modifier: Modifier = Modifier,
avatarUrl: String? = null,
canEdit: Boolean = true,
) {
var expanded by remember { mutableStateOf(false) }
Box(modifier) {
IconButton(
onClick = { expanded = true },
) {
AvatarURL(
url = avatarUrl,
contentDescription = LocalContext.current.getString(R.string.room_options_label),
)
}
DropdownMenu(
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
}
)
}
}
}
}

View file

@ -0,0 +1,41 @@
package eu.steffo.twom.composables.viewroom
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
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.composables.errorhandling.ErrorText
import eu.steffo.twom.composables.matrix.LocalSession
@Composable
fun ViewRoomContent(
modifier: Modifier = Modifier,
) {
val scope = rememberCoroutineScope()
val session = LocalSession.current
if (session == null) {
ErrorText(stringResource(R.string.error_session_missing))
return
}
Box(
modifier = modifier
.verticalScroll(rememberScrollState())
) {
Column(
modifier = Modifier.fillMaxHeight()
) {
ViewRoomTopic()
ViewRoomForm()
ViewRoomMembers()
}
}
}

View file

@ -0,0 +1,127 @@
package eu.steffo.twom.composables.viewroom
import android.util.Log
import androidx.compose.foundation.layout.Row
import androidx.compose.material3.MaterialTheme
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
import androidx.compose.ui.res.stringResource
import eu.steffo.twom.R
import eu.steffo.twom.composables.errorhandling.ErrorText
import eu.steffo.twom.composables.errorhandling.LocalizableError
import eu.steffo.twom.composables.matrix.LocalSession
import eu.steffo.twom.composables.theme.basePadding
import kotlinx.coroutines.launch
import kotlin.jvm.optionals.getOrNull
@Composable
fun ViewRoomForm() {
val scope = rememberCoroutineScope()
val session = LocalSession.current
if (session == null) {
ErrorText(stringResource(R.string.error_session_missing))
return
}
val roomRequest = LocalRoom.current
if (roomRequest == null) {
ErrorText(stringResource(R.string.room_error_room_missing))
return
}
val room = roomRequest.getOrNull()
if (room == null) {
ErrorText(stringResource(R.string.room_error_room_notfound))
return
}
// FIXME: This breaks if the member is kicked from the chat
val member = room.membershipService().getRoomMember(session.myUserId)
if (member == null) {
ErrorText(stringResource(R.string.room_error_members_notfound))
return
}
val published = observeRSVP(room = room, member = member)
var isPublishRunning by rememberSaveable { mutableStateOf(false) }
val publishError by remember { mutableStateOf(LocalizableError()) }
Row(Modifier.basePadding()) {
Text(
text = stringResource(R.string.room_rsvp_title),
style = MaterialTheme.typography.labelLarge,
)
}
RSVPForm(
published = published,
onRequestPublish = { answer, comment ->
isPublishRunning = true
publishError.clear()
scope.launch Publish@{
Log.d(
"ViewRoomForm",
"Updating RSVP with answer `$answer` and comment `$comment`..."
)
try {
room.stateService().sendStateEvent(
eventType = "eu.steffo.twom.rsvp",
stateKey = session.myUserId,
body = mapOf(
"answer" to answer.value,
"comment" to comment,
),
)
} catch (e: Throwable) {
Log.e("Room", "Failed to update eu.steffo.twom.rsvp: $publishError")
publishError.set(R.string.room_error_publish_generic, e)
isPublishRunning = false
return@Publish
}
Log.d(
"ViewRoomForm",
"Updated RSVP with answer `$answer` and comment `$comment`!"
)
if (published.event != null) {
Log.d(
"Room",
"Attempting to redact old RSVP `${published.event.eventId}`..."
)
try {
room.sendService()
.redactEvent(published.event, "Replaced with new information")
} catch (e: Throwable) {
Log.e(
"Room",
"Failed to redact old RSVP: $publishError"
)
publishError.set(R.string.room_error_redact_generic, e)
isPublishRunning = false
return@Publish
}
} else {
Log.d(
"Room",
"Not doing anything else; there isn't anything to redact."
)
}
isPublishRunning = false
}
},
isPublishRunning = isPublishRunning,
)
publishError.Show {
ErrorText(it)
}
}

View file

@ -0,0 +1,48 @@
package eu.steffo.twom.composables.viewroom
import androidx.compose.foundation.layout.Row
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import eu.steffo.twom.R
import eu.steffo.twom.composables.errorhandling.ErrorText
import eu.steffo.twom.composables.theme.basePadding
import org.matrix.android.sdk.api.session.room.members.RoomMemberQueryParams
import kotlin.jvm.optionals.getOrNull
@Composable
fun ViewRoomMembers() {
val roomRequest = LocalRoom.current
if (roomRequest == null) {
ErrorText(stringResource(R.string.room_error_room_missing))
return
}
val room = roomRequest.getOrNull()
if (room == null) {
ErrorText(stringResource(R.string.room_error_room_notfound))
return
}
val roomMembers by room.membershipService().getRoomMembersLive(
RoomMemberQueryParams.Builder().build()
).observeAsState()
if (roomMembers == null) {
ErrorText(stringResource(R.string.room_error_members_notfound))
return
}
Row(Modifier.basePadding()) {
Text(
text = stringResource(R.string.room_invitees_title),
style = MaterialTheme.typography.labelLarge,
)
}
roomMembers!!.forEach {
MemberListItem(member = it)
}
}

View file

@ -1,4 +1,4 @@
package eu.steffo.twom.room
package eu.steffo.twom.composables.viewroom
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
@ -7,16 +7,15 @@ import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Modifier
import eu.steffo.twom.matrix.LocalSession
import eu.steffo.twom.theme.TwoMTheme
import eu.steffo.twom.composables.matrix.LocalSession
import eu.steffo.twom.composables.theme.TwoMTheme
import org.matrix.android.sdk.api.session.Session
import java.util.Optional
@Composable
fun RoomActivityScaffold(
fun ViewRoomScaffold(
session: Session,
roomId: String,
onBack: () -> Unit = {},
) {
val room = Optional.ofNullable(session.roomService().getRoom(roomId))
val roomSummary by session.roomService().getRoomSummaryLive(roomId).observeAsState()
@ -27,12 +26,10 @@ fun RoomActivityScaffold(
CompositionLocalProvider(LocalRoomSummary provides roomSummary) {
Scaffold(
topBar = {
RoomActivityTopBar(
onBack = onBack,
)
ViewRoomTopBar()
},
content = {
RoomActivityContent(
ViewRoomContent(
modifier = Modifier.padding(it),
)
},

View file

@ -0,0 +1,62 @@
package eu.steffo.twom.composables.viewroom
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.LocalContentColor
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.LocalizableError
import eu.steffo.twom.composables.navigation.BackIconButton
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@Preview
fun ViewRoomTopBar(
modifier: Modifier = Modifier,
roomName: String? = null,
roomAvatarUrl: String? = null,
isLoading: Boolean = false,
error: LocalizableError? = null,
) {
TopAppBar(
modifier = modifier,
navigationIcon = {
BackIconButton()
},
title = {
if (roomName != null) {
Text(
text = roomName,
style = MaterialTheme.typography.titleLarge,
)
} else {
Text(
text = stringResource(R.string.loading),
style = MaterialTheme.typography.titleLarge,
color = LocalContentColor.current.copy(0.4f)
)
}
},
actions = {
if (isLoading) {
CircularProgressIndicator()
} else if (error != null && error.occurred()) {
ErrorIconButton(
message = error.renderString()!!
)
} else {
RoomIconButton(
avatarUrl = roomAvatarUrl,
)
}
},
)
}

View file

@ -0,0 +1,39 @@
package eu.steffo.twom.composables.viewroom
import androidx.compose.foundation.layout.Row
import androidx.compose.material3.MaterialTheme
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
import eu.steffo.twom.composables.errorhandling.ErrorText
import eu.steffo.twom.composables.theme.basePadding
@Composable
@Preview
fun ViewRoomTopic() {
val roomSummaryRequest = LocalRoomSummary.current
if (roomSummaryRequest == null) {
ErrorText(stringResource(R.string.room_error_roomsummary_missing))
return
}
val roomSummary = roomSummaryRequest.getOrNull()
if (roomSummary == null) {
ErrorText(stringResource(R.string.room_error_roomsummary_notfound))
return
}
Row(Modifier.basePadding()) {
Text(
text = stringResource(R.string.room_topic_title),
style = MaterialTheme.typography.labelLarge,
)
}
Row(Modifier.basePadding()) {
Text(roomSummary.topic)
}
}

View file

@ -0,0 +1,92 @@
package eu.steffo.twom.composables.viewroom
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import eu.steffo.twom.utils.RSVP
import eu.steffo.twom.utils.RSVPAnswer
import eu.steffo.twom.utils.TwoMGlobals
import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.session.room.Room
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
@Composable
fun observeRSVP(room: Room, member: RoomMemberSummary): RSVP {
if (member.membership == Membership.INVITE) {
return RSVP(
event = null,
answer = RSVPAnswer.PENDING,
comment = "",
)
}
val request by room.stateService().getStateEventLive(
eventType = TwoMGlobals.RSVP_STATE_TYPE,
stateKey = QueryStringValue.Equals(member.userId),
).observeAsState()
if (request == null) {
return RSVP(
event = null,
answer = RSVPAnswer.LOADING,
comment = "",
)
}
val event = request!!.getOrNull()
?: return RSVP(
event = null,
answer = RSVPAnswer.NONE,
comment = "",
)
val content = event.content
?: return RSVP(
event = event,
answer = RSVPAnswer.UNKNOWN,
comment = "",
)
val commentField = content[TwoMGlobals.RSVP_STATE_COMMENT_FIELD]
?: return RSVP(
event = event,
answer = RSVPAnswer.UNKNOWN,
comment = "",
)
val comment = commentField as? String
?: return RSVP(
event = event,
answer = RSVPAnswer.UNKNOWN,
comment = "",
)
val answerField = content[TwoMGlobals.RSVP_STATE_ANSWER_FIELD]
?: return RSVP(
event = event,
answer = RSVPAnswer.UNKNOWN,
comment = comment,
)
val answerString = answerField as? String
?: return RSVP(
event = event,
answer = RSVPAnswer.UNKNOWN,
comment = comment,
)
val answer = when (answerString) {
RSVPAnswer.SURE.value -> RSVPAnswer.SURE
RSVPAnswer.LATER.value -> RSVPAnswer.LATER
RSVPAnswer.MAYBE.value -> RSVPAnswer.MAYBE
RSVPAnswer.NOWAY.value -> RSVPAnswer.NOWAY
else -> RSVPAnswer.UNKNOWN
}
return RSVP(
event = event,
answer = answer,
comment = comment,
)
}

View file

@ -1,76 +0,0 @@
package eu.steffo.twom.room
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
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.matrix.LocalSession
import eu.steffo.twom.matrix.avatar.AvatarFromURL
import org.matrix.android.sdk.api.session.getUser
// TODO: Check this with brain on
@Composable
fun MemberListItem(
modifier: Modifier = Modifier,
memberId: String,
onClickMember: (memberId: String) -> Unit = {},
rsvpAnswer: RSVPAnswer,
rsvpComment: String,
) {
val session = LocalSession.current
val user = session?.getUser(memberId)
val icon = rsvpAnswer.toIcon()
val responseResourceId = rsvpAnswer.toResponseResourceId()
val colorRole = rsvpAnswer.toStaticColorRole()
ListItem(
modifier = modifier.clickable {
onClickMember(memberId)
},
headlineContent = {
Text(
text = user?.displayName ?: stringResource(R.string.user_unresolved_name),
)
},
leadingContent = {
Box(
Modifier
.padding(end = 10.dp)
.size(40.dp)
.clip(MaterialTheme.shapes.extraLarge)
) {
AvatarFromURL(
url = user?.avatarUrl,
)
}
},
trailingContent = {
Icon(
imageVector = icon,
contentDescription = stringResource(responseResourceId),
tint = colorRole.value,
)
},
supportingContent = {
if (rsvpComment != "") {
Text(
text = rsvpComment,
color = colorRole.value,
)
}
},
)
}

View file

@ -1,118 +0,0 @@
package eu.steffo.twom.room
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Cancel
import androidx.compose.material.icons.outlined.CheckCircle
import androidx.compose.material.icons.outlined.Circle
import androidx.compose.material.icons.outlined.Help
import androidx.compose.material.icons.outlined.Schedule
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.graphics.vector.ImageVector
import eu.steffo.twom.R
import eu.steffo.twom.theme.StaticColorRole
import eu.steffo.twom.theme.colorRoleLater
import eu.steffo.twom.theme.colorRoleMaybe
import eu.steffo.twom.theme.colorRoleNoway
import eu.steffo.twom.theme.colorRoleSure
import eu.steffo.twom.theme.colorRoleUnknown
import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.room.Room
import org.matrix.android.sdk.api.util.Optional
enum class RSVPAnswer {
SURE,
LATER,
MAYBE,
NOWAY,
UNKNOWN,
}
@Composable
fun RSVPAnswer.toStaticColorRole(): StaticColorRole {
return when (this) {
RSVPAnswer.SURE -> colorRoleSure()
RSVPAnswer.LATER -> colorRoleLater()
RSVPAnswer.MAYBE -> colorRoleMaybe()
RSVPAnswer.NOWAY -> colorRoleNoway()
RSVPAnswer.UNKNOWN -> colorRoleUnknown()
}
}
fun RSVPAnswer.toIcon(): ImageVector {
return when (this) {
RSVPAnswer.SURE -> Icons.Outlined.CheckCircle
RSVPAnswer.LATER -> Icons.Outlined.Schedule
RSVPAnswer.MAYBE -> Icons.Outlined.Help
RSVPAnswer.NOWAY -> Icons.Outlined.Cancel
RSVPAnswer.UNKNOWN -> Icons.Outlined.Circle
}
}
fun RSVPAnswer.toLabelResourceId(): Int {
return when (this) {
RSVPAnswer.SURE -> R.string.room_rsvp_sure_label
RSVPAnswer.LATER -> R.string.room_rsvp_later_label
RSVPAnswer.MAYBE -> R.string.room_rsvp_maybe_label
RSVPAnswer.NOWAY -> R.string.room_rsvp_noway_label
RSVPAnswer.UNKNOWN -> R.string.room_rsvp_unknown_label
}
}
fun RSVPAnswer.toResponseResourceId(): Int {
return when (this) {
RSVPAnswer.SURE -> R.string.room_rsvp_sure_response
RSVPAnswer.LATER -> R.string.room_rsvp_later_response
RSVPAnswer.MAYBE -> R.string.room_rsvp_maybe_response
RSVPAnswer.NOWAY -> R.string.room_rsvp_noway_response
RSVPAnswer.UNKNOWN -> R.string.room_rsvp_unknown_response
}
}
fun RSVPAnswer.toPlaceholderResourceId(): Int {
return when (this) {
RSVPAnswer.SURE -> R.string.room_rsvp_sure_placeholder
RSVPAnswer.LATER -> R.string.room_rsvp_later_placeholder
RSVPAnswer.MAYBE -> R.string.room_rsvp_maybe_placeholder
RSVPAnswer.NOWAY -> R.string.room_rsvp_noway_placeholder
RSVPAnswer.UNKNOWN -> R.string.room_rsvp_unknown_placeholder
}
}
fun makeRSVP(request: State<Optional<Event>?>?): Triple<Event, RSVPAnswer, String>? {
val event = request?.value?.getOrNull() ?: return null
val content = event.content ?: return null
val answerAny = content["answer"]
val commentAny = content["comment"]
val answer = if (answerAny is String) {
try {
RSVPAnswer.valueOf(answerAny)
} catch (_: IllegalArgumentException) {
RSVPAnswer.UNKNOWN
}
} else {
RSVPAnswer.UNKNOWN
}
val comment = if (commentAny is String) {
commentAny
} else {
""
}
return Triple(event, answer, comment)
}
@Composable
fun observeRsvpAsLiveState(room: Room, userId: String): Triple<Event, RSVPAnswer, String>? {
val stateRequest = room.stateService().getStateEventLive(
eventType = "eu.steffo.twom.rsvp",
stateKey = QueryStringValue.Equals(userId),
).observeAsState()
return makeRSVP(stateRequest)
}

View file

@ -1,43 +0,0 @@
package eu.steffo.twom.room
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.OutlinedTextFieldDefaults
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
@Composable
@Preview
fun RSVPCommentField(
modifier: Modifier = Modifier,
value: String = "",
onChange: (value: String) -> Unit = {},
rsvpAnswer: RSVPAnswer = RSVPAnswer.UNKNOWN,
) {
val colorRole = rsvpAnswer.toStaticColorRole()
OutlinedTextField(
modifier = modifier,
value = value,
onValueChange = onChange,
singleLine = true,
shape = MaterialTheme.shapes.small,
placeholder = {
Text(
text = stringResource(rsvpAnswer.toPlaceholderResourceId())
)
},
colors = OutlinedTextFieldDefaults.colors(
focusedContainerColor = colorRole.valueContainer,
unfocusedContainerColor = colorRole.valueContainer,
focusedTextColor = colorRole.onValueContainer,
unfocusedTextColor = colorRole.onValueContainer,
focusedBorderColor = colorRole.onValueContainer,
unfocusedBorderColor = colorRole.onValueContainer.copy(alpha = 0.3f),
cursorColor = colorRole.onValueContainer,
)
)
}

View file

@ -1,47 +0,0 @@
package eu.steffo.twom.room
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@Composable
@Preview
fun RoomActivityAnswerForm(
currentRsvpAnswer: RSVPAnswer = RSVPAnswer.UNKNOWN,
currentRsvpComment: String = "",
onUpdate: (rsvpAnswer: RSVPAnswer, rsvpComment: String) -> Unit = { _, _ -> },
isUpdating: Boolean = false,
) {
var rsvpAnswer by rememberSaveable { mutableStateOf(currentRsvpAnswer) }
var rsvpComment by rememberSaveable { mutableStateOf(currentRsvpComment) }
val hasChanged = (rsvpAnswer != currentRsvpAnswer || rsvpComment != currentRsvpComment)
RSVPAnswerSelectRow(
value = rsvpAnswer,
onChange = { rsvpAnswer = it },
)
RSVPCommentField(
modifier = Modifier
.padding(start = 10.dp, end = 10.dp)
.fillMaxWidth(),
value = rsvpComment,
onChange = { rsvpComment = it },
rsvpAnswer = rsvpAnswer,
)
RSVPUpdateButton(
modifier = Modifier
.padding(start = 10.dp, end = 10.dp, top = 4.dp, bottom = 4.dp)
.fillMaxWidth(),
onClick = { onUpdate(rsvpAnswer, rsvpComment) },
enabled = hasChanged && !isUpdating,
rsvpAnswer = rsvpAnswer,
)
}

View file

@ -1,258 +0,0 @@
package eu.steffo.twom.room
import android.util.Log
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
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.res.stringResource
import eu.steffo.twom.R
import eu.steffo.twom.matrix.LocalSession
import eu.steffo.twom.theme.ErrorText
import eu.steffo.twom.theme.TwoMPadding
import kotlinx.coroutines.launch
import kotlin.jvm.optionals.getOrNull
@Composable
fun RoomActivityContent(
modifier: Modifier = Modifier,
) {
val scope = rememberCoroutineScope()
val session = LocalSession.current
if (session == null) {
ErrorText(stringResource(R.string.error_session_missing))
return
}
val roomRequest = LocalRoom.current
if (roomRequest == null) {
ErrorText(stringResource(R.string.room_error_room_missing))
return
}
val room = roomRequest.getOrNull()
if (room == null) {
ErrorText(stringResource(R.string.room_error_room_notfound))
return
}
val roomSummaryRequest = LocalRoomSummary.current
if (roomSummaryRequest == null) {
ErrorText(stringResource(R.string.room_error_roomsummary_missing))
return
}
val roomSummary = roomSummaryRequest.getOrNull()
if (roomSummary == null) {
ErrorText(stringResource(R.string.room_error_roomsummary_notfound))
return
}
LaunchedEffect(roomSummary.otherMemberIds) ResolveUnknownUsers@{
// Resolve unknown users, one at a time
roomSummary.otherMemberIds.map {
if (session.userService().getUser(it) == null) {
Log.i("Room", "Resolving unknown user: $it")
session.userService().resolveUser(it)
Log.d("Room", "Successfully resolved unknown user: $it")
} else {
Log.v("Room", "Not resolving known user: $it")
}
}
}
val myRsvpRequest = observeRsvpAsLiveState(room = room, userId = session.myUserId)
val otherRsvpRequests =
roomSummary.otherMemberIds.map { it to observeRsvpAsLiveState(room = room, userId = it) }
var isUpdatingMyRsvp by rememberSaveable { mutableStateOf(false) }
var errorMyRsvp by rememberSaveable { mutableStateOf<Throwable?>(null) }
var isSendingInvite by rememberSaveable { mutableStateOf(false) }
var errorInvite by rememberSaveable { mutableStateOf<Throwable?>(null) }
Box(
modifier = modifier
.verticalScroll(rememberScrollState())
) {
Column(
modifier = Modifier.fillMaxHeight()
) {
if (roomSummary.topic != "") {
Row(TwoMPadding.base) {
Text(
text = stringResource(R.string.room_topic_title),
style = MaterialTheme.typography.labelLarge,
)
}
Row(TwoMPadding.base) {
Text(roomSummary.topic)
}
}
Row(TwoMPadding.base) {
Text(
text = stringResource(R.string.room_rsvp_title),
style = MaterialTheme.typography.labelLarge,
)
}
RoomActivityAnswerForm(
// FIXME: This always set the request to UNKNOWN
currentRsvpAnswer = myRsvpRequest?.second ?: RSVPAnswer.UNKNOWN,
currentRsvpComment = myRsvpRequest?.third ?: "",
onUpdate = { answer, comment ->
isUpdatingMyRsvp = true
errorMyRsvp = null
scope.launch SendRSVP@{
Log.d(
"Room",
"Updating eu.steffo.twom.rsvp with answer `$answer` and comment `$comment`..."
)
try {
room.stateService().sendStateEvent(
eventType = "eu.steffo.twom.rsvp",
stateKey = session.myUserId,
body = mapOf(
pairs = arrayOf(
"answer" to answer.toString(),
"comment" to comment,
)
),
)
} catch (error: Exception) {
Log.e("Room", "Failed to update eu.steffo.twom.rsvp: $error")
errorMyRsvp = error
isUpdatingMyRsvp = false
return@SendRSVP
}
Log.d(
"Room",
"Updated eu.steffo.twom.rsvp with answer `$answer` and comment `$comment`!"
)
if (myRsvpRequest != null) {
val myRsvpRequestEventId = myRsvpRequest.first.eventId
Log.d(
"Room",
"Attempting to redact old eu.steffo.twom.rsvp event `${myRsvpRequestEventId}`..."
)
try {
room.sendService()
.redactEvent(
myRsvpRequest.first,
"Replaced with new information"
)
} catch (error: Throwable) {
Log.e(
"Room",
"Failed to redact the old eu.steffo.twom.rsvp: $error"
)
errorMyRsvp = error
isUpdatingMyRsvp = false
return@SendRSVP
}
} else {
Log.d(
"Room",
"Not doing anything else; there isn't anything to redact."
)
}
isUpdatingMyRsvp = false
}
},
isUpdating = isUpdatingMyRsvp,
)
if (errorMyRsvp != null) {
// TODO: Maybe add an human-friendly error message?
Row(TwoMPadding.base) {
ErrorText(
errorMyRsvp.toString()
)
}
}
Row(TwoMPadding.base) {
Text(
text = stringResource(R.string.room_invitees_title),
style = MaterialTheme.typography.labelLarge,
)
}
Column(TwoMPadding.base) {
MemberListItem(
memberId = LocalSession.current!!.myUserId,
rsvpAnswer = myRsvpRequest?.second ?: RSVPAnswer.UNKNOWN,
rsvpComment = myRsvpRequest?.third ?: "",
)
// FIXME: This also displays invited members!
otherRsvpRequests.forEach {
MemberListItem(
memberId = it.first,
rsvpAnswer = it.second?.second ?: RSVPAnswer.UNKNOWN,
rsvpComment = it.second?.third ?: "",
)
}
}
Row(TwoMPadding.base) {
Text(
text = stringResource(R.string.room_invite_title),
style = MaterialTheme.typography.labelLarge,
)
}
Row(TwoMPadding.base) {
RoomActivityInviteForm(
busy = isSendingInvite,
onSend = {
scope.launch SendInvite@{
isSendingInvite = true
errorInvite = null
Log.d("Room", "Sending invite to `$it`...")
try {
room.membershipService().invite(it)
} catch (error: Throwable) {
Log.e("Room", "Failed to send invite to `$it`: $error")
errorInvite = error
isSendingInvite = false
return@SendInvite
}
Log.d("Room", "Successfully sent invite to `$it`!")
isSendingInvite = false
}
}
)
}
if (errorInvite != null) {
// TODO: Maybe add an human-friendly error message?
Row(TwoMPadding.base) {
ErrorText(
errorInvite.toString()
)
}
}
}
}
}

View file

@ -1,76 +0,0 @@
package eu.steffo.twom.room
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.OutlinedTextFieldDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import eu.steffo.twom.R
import eu.steffo.twom.theme.colorRoleUnknown
@Composable
@Preview
fun RoomActivityInviteForm(
modifier: Modifier = Modifier,
busy: Boolean = false,
onSend: (userId: String) -> Unit = {},
) {
var value by rememberSaveable { mutableStateOf("") }
val colorRole = colorRoleUnknown()
Column(modifier) {
OutlinedTextField(
modifier = Modifier
.fillMaxWidth(),
value = value,
onValueChange = { value = it },
singleLine = true,
shape = MaterialTheme.shapes.small,
placeholder = {
Text(
text = stringResource(R.string.room_invite_username_placeholder)
)
},
colors = OutlinedTextFieldDefaults.colors(
focusedContainerColor = colorRole.valueContainer,
unfocusedContainerColor = colorRole.valueContainer,
focusedTextColor = colorRole.onValueContainer,
unfocusedTextColor = colorRole.onValueContainer,
focusedBorderColor = colorRole.onValueContainer,
unfocusedBorderColor = colorRole.onValueContainer.copy(alpha = 0.3f),
cursorColor = colorRole.onValueContainer,
)
)
Button(
modifier = Modifier
.padding(top = 4.dp)
.fillMaxWidth(),
onClick = { onSend(value) },
shape = MaterialTheme.shapes.small,
// FIXME: Maybe I should validate usernames with a regex
enabled = (value.contains("@") && value.contains(":") && !busy),
colors = ButtonDefaults.buttonColors(
containerColor = colorRole.value,
contentColor = colorRole.onValue,
)
) {
Text(
text = stringResource(R.string.room_invite_button_label)
)
}
}
}

View file

@ -1,71 +0,0 @@
package eu.steffo.twom.room
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.minimumInteractiveComponentSize
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.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.dp
import eu.steffo.twom.R
import eu.steffo.twom.matrix.avatar.AvatarFromURL
@Composable
fun RoomActivityRoomIconButton(
modifier: Modifier = Modifier,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
avatarUrl: String,
) {
var expanded by remember { mutableStateOf(false) }
Box(modifier) {
// Mostly copied from IconButton's source
// TODO: Make sure accessibility works right
Box(
modifier = Modifier
.minimumInteractiveComponentSize()
.size(40.dp)
.clip(MaterialTheme.shapes.medium)
.clickable(
role = Role.Button,
interactionSource = interactionSource,
indication = rememberRipple(
bounded = false,
radius = 28.dp
)
) { expanded = true },
) {
AvatarFromURL(
url = avatarUrl,
contentDescription = LocalContext.current.getString(R.string.room_options_label),
)
}
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false },
) {
DropdownMenuItem(
text = {
Text("garasauto")
},
onClick = {
expanded = false
}
)
}
}
}

View file

@ -1,60 +0,0 @@
package eu.steffo.twom.room
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
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.platform.LocalContext
import androidx.compose.ui.res.stringResource
import eu.steffo.twom.R
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun RoomActivityTopBar(
modifier: Modifier = Modifier,
onBack: () -> Unit = {},
) {
val isLoading = (LocalRoomSummary.current == null)
val roomSummary = LocalRoomSummary.current?.getOrNull()
val isError = (!isLoading && roomSummary == null)
TopAppBar(
modifier = modifier,
navigationIcon = {
IconButton(onClick = onBack) {
Icon(
imageVector = Icons.Filled.ArrowBack,
contentDescription = LocalContext.current.getString(R.string.back)
)
}
},
title = {
if (roomSummary != null) {
Text(
text = roomSummary.displayName,
style = MaterialTheme.typography.titleLarge,
)
}
},
actions = {
if (isLoading) {
CircularProgressIndicator()
} else if (isError) {
Icon(Icons.Filled.Warning, stringResource(R.string.error))
} else {
RoomActivityRoomIconButton(
avatarUrl = roomSummary!!.avatarUrl,
)
}
},
)
}

View file

@ -1,15 +0,0 @@
package eu.steffo.twom.theme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@Composable
fun ErrorText(
text: String
) {
Text(
text = text,
color = MaterialTheme.colorScheme.error,
)
}

View file

@ -1,10 +0,0 @@
package eu.steffo.twom.theme
import androidx.compose.ui.graphics.Color
data class StaticColorRole(
val value: Color,
val onValue: Color,
val valueContainer: Color,
val onValueContainer: Color,
)

View file

@ -1,11 +0,0 @@
package eu.steffo.twom.theme
import androidx.compose.foundation.layout.padding
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
object TwoMPadding {
val base = Modifier.padding(all = 10.dp)
val chips = Modifier.padding(start = 2.5.dp, end = 2.5.dp)
}

View file

@ -1,29 +0,0 @@
package eu.steffo.twom.theme
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import com.google.android.material.color.MaterialColors
@Composable
fun colorRoleLater(): StaticColorRole {
val ctx = LocalContext.current
return when (isSystemInDarkTheme()) {
false -> StaticColorRole(
value = Color(MaterialColors.harmonizeWithPrimary(ctx, 0x00658B)),
onValue = Color(MaterialColors.harmonizeWithPrimary(ctx, 0xFFFFFF)),
valueContainer = Color(MaterialColors.harmonizeWithPrimary(ctx, 0xC4E7FF)),
onValueContainer = Color(MaterialColors.harmonizeWithPrimary(ctx, 0x001E2C)),
)
true -> StaticColorRole(
value = Color(MaterialColors.harmonizeWithPrimary(ctx, 0x7DD0FF)),
onValue = Color(MaterialColors.harmonizeWithPrimary(ctx, 0x00344A)),
valueContainer = Color(MaterialColors.harmonizeWithPrimary(ctx, 0x004C69)),
onValueContainer = Color(MaterialColors.harmonizeWithPrimary(ctx, 0xC4E7FF)),
)
}
}

View file

@ -1,29 +0,0 @@
package eu.steffo.twom.theme
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import com.google.android.material.color.MaterialColors
@Composable
fun colorRoleMaybe(): StaticColorRole {
val ctx = LocalContext.current
return when (isSystemInDarkTheme()) {
false -> StaticColorRole(
value = Color(MaterialColors.harmonizeWithPrimary(ctx, 0x765B00)),
onValue = Color(MaterialColors.harmonizeWithPrimary(ctx, 0xFFFFFF)),
valueContainer = Color(MaterialColors.harmonizeWithPrimary(ctx, 0xFFDF94)),
onValueContainer = Color(MaterialColors.harmonizeWithPrimary(ctx, 0x241A00)),
)
true -> StaticColorRole(
value = Color(MaterialColors.harmonizeWithPrimary(ctx, 0xEDC148)),
onValue = Color(MaterialColors.harmonizeWithPrimary(ctx, 0x3E2E00)),
valueContainer = Color(MaterialColors.harmonizeWithPrimary(ctx, 0x594400)),
onValueContainer = Color(MaterialColors.harmonizeWithPrimary(ctx, 0xFFDF94)),
)
}
}

View file

@ -1,29 +0,0 @@
package eu.steffo.twom.theme
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import com.google.android.material.color.MaterialColors
@Composable
fun colorRoleNoway(): StaticColorRole {
val ctx = LocalContext.current
return when (isSystemInDarkTheme()) {
false -> StaticColorRole(
value = Color(MaterialColors.harmonizeWithPrimary(ctx, 0xAB3520)),
onValue = Color(MaterialColors.harmonizeWithPrimary(ctx, 0xFFFFFF)),
valueContainer = Color(MaterialColors.harmonizeWithPrimary(ctx, 0xFFDAD3)),
onValueContainer = Color(MaterialColors.harmonizeWithPrimary(ctx, 0x3F0400)),
)
true -> StaticColorRole(
value = Color(MaterialColors.harmonizeWithPrimary(ctx, 0xFFB4A5)),
onValue = Color(MaterialColors.harmonizeWithPrimary(ctx, 0x650A00)),
valueContainer = Color(MaterialColors.harmonizeWithPrimary(ctx, 0x891D0A)),
onValueContainer = Color(MaterialColors.harmonizeWithPrimary(ctx, 0xFFDAD3)),
)
}
}

View file

@ -1,28 +0,0 @@
package eu.steffo.twom.theme
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import com.google.android.material.color.MaterialColors
@Composable
fun colorRoleSure(): StaticColorRole {
val ctx = LocalContext.current
return when (isSystemInDarkTheme()) {
false -> StaticColorRole(
value = Color(MaterialColors.harmonizeWithPrimary(ctx, 0x006E2C)),
onValue = Color(MaterialColors.harmonizeWithPrimary(ctx, 0xFFFFFF)),
valueContainer = Color(MaterialColors.harmonizeWithPrimary(ctx, 0x7FFC95)),
onValueContainer = Color(MaterialColors.harmonizeWithPrimary(ctx, 0x002108)),
)
true -> StaticColorRole(
value = Color(MaterialColors.harmonizeWithPrimary(ctx, 0x62DF7C)),
onValue = Color(MaterialColors.harmonizeWithPrimary(ctx, 0x003913)),
valueContainer = Color(MaterialColors.harmonizeWithPrimary(ctx, 0x00531F)),
onValueContainer = Color(MaterialColors.harmonizeWithPrimary(ctx, 0x7FFC95)),
)
}
}

View file

@ -1,14 +0,0 @@
package eu.steffo.twom.theme
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
@Composable
fun colorRoleUnknown(): StaticColorRole {
return StaticColorRole(
value = MaterialTheme.colorScheme.inverseSurface,
onValue = MaterialTheme.colorScheme.inverseOnSurface,
valueContainer = MaterialTheme.colorScheme.surface,
onValueContainer = MaterialTheme.colorScheme.onSurface,
)
}

View file

@ -0,0 +1,9 @@
package eu.steffo.twom.utils
import org.matrix.android.sdk.api.session.events.model.Event
data class RSVP(
val event: Event?,
val answer: RSVPAnswer,
val comment: String,
)

View file

@ -0,0 +1,227 @@
package eu.steffo.twom.utils
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.BuildCircle
import androidx.compose.material.icons.outlined.Cancel
import androidx.compose.material.icons.outlined.CheckCircle
import androidx.compose.material.icons.outlined.Circle
import androidx.compose.material.icons.outlined.HelpOutline
import androidx.compose.material.icons.outlined.HourglassEmpty
import androidx.compose.material.icons.outlined.MoreHoriz
import androidx.compose.material.icons.outlined.Schedule
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import eu.steffo.twom.R
import eu.steffo.twom.composables.theme.LaterColorRole
import eu.steffo.twom.composables.theme.MaybeColorRole
import eu.steffo.twom.composables.theme.NowayColorRole
import eu.steffo.twom.composables.theme.NullishColorRole
import eu.steffo.twom.composables.theme.StaticColorRole
import eu.steffo.twom.composables.theme.SureColorRole
enum class RSVPAnswer {
// Will be there!
SURE {
override val value: String
get() = "SURE"
override val staticColorRole: StaticColorRole
get() = SureColorRole
override val icon: ImageVector
get() = Icons.Outlined.CheckCircle
@Composable
override fun toLabel(): String =
stringResource(R.string.room_rsvp_sure_label)
@Composable
override fun toResponse(): String =
stringResource(R.string.room_rsvp_sure_response)
@Composable
override fun toCommentPlaceholder(): String =
stringResource(R.string.room_rsvp_sure_placeholder)
},
// Will be there, but later!
LATER {
override val value: String
get() = "LATER"
override val staticColorRole: StaticColorRole
get() = LaterColorRole
override val icon: ImageVector
get() = Icons.Outlined.Schedule
@Composable
override fun toLabel(): String =
stringResource(R.string.room_rsvp_later_label)
@Composable
override fun toResponse(): String =
stringResource(R.string.room_rsvp_later_response)
@Composable
override fun toCommentPlaceholder(): String =
stringResource(R.string.room_rsvp_later_placeholder)
},
// Might be there...
MAYBE {
override val value: String
get() = "MAYBE"
override val staticColorRole: StaticColorRole
get() = MaybeColorRole
override val icon: ImageVector
get() = Icons.Outlined.HelpOutline
@Composable
override fun toLabel(): String =
stringResource(R.string.room_rsvp_maybe_label)
@Composable
override fun toResponse(): String =
stringResource(R.string.room_rsvp_maybe_response)
@Composable
override fun toCommentPlaceholder(): String =
stringResource(R.string.room_rsvp_maybe_placeholder)
},
// Won't be there.
NOWAY {
override val value: String
get() = "NOWAY"
override val staticColorRole: StaticColorRole
get() = NowayColorRole
override val icon: ImageVector
get() = Icons.Outlined.Cancel
@Composable
override fun toLabel(): String =
stringResource(R.string.room_rsvp_noway_label)
@Composable
override fun toResponse(): String =
stringResource(R.string.room_rsvp_noway_response)
@Composable
override fun toCommentPlaceholder(): String =
stringResource(R.string.room_rsvp_noway_placeholder)
},
// An option differing from the previous ones.
UNKNOWN {
override val value: String?
get() = null
override val staticColorRole: StaticColorRole
get() = NullishColorRole
override val icon: ImageVector
get() = Icons.Outlined.BuildCircle
@Composable
override fun toLabel(): String? =
null
@Composable
override fun toResponse(): String =
stringResource(R.string.room_rsvp_unknown_response)
@Composable
override fun toCommentPlaceholder(): String =
stringResource(R.string.room_rsvp_nullish_placeholder)
},
// The answer is still being loaded.
LOADING {
override val value: String?
get() = null
override val staticColorRole: StaticColorRole
get() = NullishColorRole
override val icon: ImageVector
get() = Icons.Outlined.HourglassEmpty
@Composable
override fun toLabel(): String? = null
@Composable
override fun toResponse(): String =
stringResource(R.string.loading)
@Composable
override fun toCommentPlaceholder(): String =
stringResource(R.string.room_rsvp_nullish_placeholder)
},
// No answer has been provided yet.
NONE {
override val value: String?
get() = null
override val staticColorRole: StaticColorRole
get() = NullishColorRole
override val icon: ImageVector
get() = Icons.Outlined.Circle
@Composable
override fun toLabel(): String? =
null
@Composable
override fun toResponse(): String =
stringResource(R.string.room_rsvp_none_response)
@Composable
override fun toCommentPlaceholder(): String =
stringResource(R.string.room_rsvp_nullish_placeholder)
},
// Has been invited, but has not accepted yet.
PENDING {
override val value: String?
get() = null
override val staticColorRole: StaticColorRole
get() = NullishColorRole
override val icon: ImageVector
get() = Icons.Outlined.MoreHoriz
@Composable
override fun toLabel(): String? =
null
@Composable
override fun toResponse(): String =
stringResource(R.string.room_rsvp_pending_response)
@Composable
override fun toCommentPlaceholder(): String =
stringResource(R.string.room_rsvp_nullish_placeholder)
};
abstract val value: String?
abstract val staticColorRole: StaticColorRole
abstract val icon: ImageVector
@Composable
abstract fun toLabel(): String?
@Composable
abstract fun toResponse(): String
@Composable
abstract fun toCommentPlaceholder(): String
}

View file

@ -1,4 +1,4 @@
package eu.steffo.twom.matrix
package eu.steffo.twom.utils
import android.content.Context
import android.util.Log
@ -9,7 +9,7 @@ import org.matrix.android.sdk.api.MatrixConfiguration
/**
* Object containing the global state of the application.
*/
object TwoMMatrix {
object TwoMGlobals {
/**
* The global [Matrix] object of the application.
*
@ -38,4 +38,10 @@ object TwoMMatrix {
}
const val ROOM_TYPE = "eu.steffo.twom.happening"
const val RSVP_STATE_TYPE = "eu.steffo.twom.rsvp"
const val RSVP_STATE_ANSWER_FIELD = "answer"
const val RSVP_STATE_COMMENT_FIELD = "comment"
}

View file

@ -1,4 +1,4 @@
package eu.steffo.twom.matrix
package eu.steffo.twom.utils
import android.content.Context
import eu.steffo.twom.R

View file

@ -52,13 +52,10 @@
<string name="room_rsvp_later_placeholder">Why will you be late?</string>
<string name="room_rsvp_maybe_placeholder">What will determine your partecipation?</string>
<string name="room_rsvp_noway_placeholder">Why won\'t you partecipate?</string>
<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_rsvp_update_label">Update</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>
<string name="room_error_roomsummary_missing">The Matrix room summary context has not been initialized.</string>
<string name="room_error_roomsummary_notfound">Could not find the requested Matrix room summary.</string>
<string name="room_invite_username_placeholder">\@steffotwo:candy.steffo.eu</string>
@ -74,4 +71,15 @@
<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>
<string name="close">Close</string>
<string name="room_options_edit_text">Edit party</string>
<string name="room_options_zoom_text">Zoom on avatar</string>
<string name="room_error_members_notfound">Could not retrieve the list of room members.</string>
<string name="room_rsvp_nullish_placeholder">Leave a comment...</string>
<string name="room_rsvp_unknown_response">Has given an unsupported response</string>
<string name="room_rsvp_none_response">Hasn\'t responded yet</string>
<string name="room_rsvp_pending_response">Hasn\'t opened the invite yet</string>
<string name="room_uninvite_label">Uninvite</string>
<string name="room_error_publish_generic">Something went wrong while updating your RSVP: %1$s</string>
<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>
</resources>

View file

@ -3,6 +3,6 @@
<style name="Theme.TwoM" parent="@style/Theme.Material3.DayNight" />
<style name="Theme.TwoM.Dialog" parent="@style/Theme.Material3.DayNight.Dialog" />
<style name="Theme.TwoM.BottomSheetDialog" parent="@style/Theme.Material3.DayNight.BottomSheetDialog" />
</resources>