1
Fork 0
mirror of https://github.com/Steffo99/twom.git synced 2024-11-22 08:04:26 +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"> android:theme="@style/Theme.TwoM">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
@ -46,13 +45,17 @@
android:theme="@style/Theme.TwoM" /> android:theme="@style/Theme.TwoM" />
<activity <activity
android:name=".activities.RoomActivity" android:name=".activities.ViewRoomActivity"
android:theme="@style/Theme.TwoM" /> android:theme="@style/Theme.TwoM" />
<activity <activity
android:name=".activities.CreateRoomActivity" android:name=".activities.CreateRoomActivity"
android:theme="@style/Theme.TwoM" /> android:theme="@style/Theme.TwoM" />
<activity
android:name=".activities.InviteUserActivity"
android:theme="@style/Theme.TwoM.BottomSheetDialog" />
</application> </application>
</manifest> </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 LoginActivity : ComponentActivity() {
class Contract : ActivityResultContract<Unit, Unit>() { class Contract : ActivityResultContract<Unit, Unit?>() {
override fun createIntent(context: Context, input: Unit): Intent { override fun createIntent(context: Context, input: Unit): Intent {
return Intent(context, LoginActivity::class.java) 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() { override fun onStart() {

View file

@ -7,12 +7,10 @@ import android.util.Log
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.result.ActivityResult import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.net.toFile import androidx.core.net.toFile
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import eu.steffo.twom.composables.main.MainScaffold import eu.steffo.twom.composables.main.MainScaffold
import eu.steffo.twom.matrix.TwoMMatrix import eu.steffo.twom.utils.TwoMGlobals
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams 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() { 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 private var session: Session? = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
TwoMMatrix.ensureMatrix(applicationContext) TwoMGlobals.ensureMatrix(applicationContext)
fetchLastSession() fetchLastSession()
openSession() openSession()
loginLauncher =
registerForActivityResult(
ActivityResultContracts.StartActivityForResult(),
this::onLogin
)
roomLauncher =
registerForActivityResult(
ActivityResultContracts.StartActivityForResult(),
this::onRoom
)
createLauncher =
registerForActivityResult(
ActivityResultContracts.StartActivityForResult(),
this::onCreate
)
resetContent() resetContent()
} }
@ -64,7 +39,7 @@ class MainActivity : ComponentActivity() {
private fun fetchLastSession() { private fun fetchLastSession() {
Log.d("Main", "Fetching the last successfully authenticated session...") Log.d("Main", "Fetching the last successfully authenticated session...")
session = TwoMMatrix.matrix.authenticationService().getLastAuthenticatedSession() session = TwoMGlobals.matrix.authenticationService().getLastAuthenticatedSession()
} }
private fun openSession() { 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) { private fun onCreate(result: ActivityResult) {
Log.d("Main", "Received result from create activity: $result") Log.d("Main", "Received result from create activity: $result")
if (result.resultCode == RESULT_OK) { if (result.resultCode == RESULT_OK) {
@ -146,7 +100,7 @@ class MainActivity : ComponentActivity() {
createRoomParams.name = name createRoomParams.name = name
createRoomParams.topic = description createRoomParams.topic = description
createRoomParams.preset = CreateRoomPreset.PRESET_PRIVATE_CHAT createRoomParams.preset = CreateRoomPreset.PRESET_PRIVATE_CHAT
createRoomParams.roomType = TwoMMatrix.ROOM_TYPE createRoomParams.roomType = TwoMGlobals.ROOM_TYPE
createRoomParams.initialStates = mutableListOf( createRoomParams.initialStates = mutableListOf(
CreateRoomStateEvent( CreateRoomStateEvent(
type = "m.room.power_levels", type = "m.room.power_levels",

View file

@ -7,18 +7,18 @@ import android.util.Log
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContract import androidx.activity.result.contract.ActivityResultContract
import eu.steffo.twom.matrix.TwoMMatrix import eu.steffo.twom.composables.viewroom.ViewRoomScaffold
import eu.steffo.twom.room.RoomActivityScaffold import eu.steffo.twom.utils.TwoMGlobals
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
class RoomActivity : ComponentActivity() { class ViewRoomActivity : ComponentActivity() {
companion object { companion object {
const val ROOM_ID_EXTRA = "roomId" const val ROOM_ID_EXTRA = "roomId"
} }
class Contract : ActivityResultContract<String, Unit>() { class Contract : ActivityResultContract<String, Unit>() {
override fun createIntent(context: Context, input: String): Intent { 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) intent.putExtra(ROOM_ID_EXTRA, input)
return intent return intent
} }
@ -31,7 +31,7 @@ class RoomActivity : ComponentActivity() {
private fun fetchLastSession() { private fun fetchLastSession() {
Log.d("Main", "Fetching the last successfully authenticated session...") 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... // 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?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -47,13 +47,9 @@ class RoomActivity : ComponentActivity() {
val roomId = intent.getStringExtra(ROOM_ID_EXTRA) val roomId = intent.getStringExtra(ROOM_ID_EXTRA)
setContent { setContent {
RoomActivityScaffold( ViewRoomScaffold(
session = session, session = session,
roomId = roomId!!, // FIXME: Again, this should be set. Should. 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.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@ -8,16 +8,13 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
@Composable @Composable
@Preview(widthDp = 40, heightDp = 40) @Preview(widthDp = 40, heightDp = 40)
fun AvatarFromDefault( fun AvatarEmpty(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
fallbackText: String = "?", text: String? = null,
contentDescription: String = "",
) { ) {
Box( Box(
modifier = modifier modifier = modifier
@ -26,12 +23,9 @@ fun AvatarFromDefault(
) { ) {
Text( Text(
modifier = Modifier modifier = Modifier
.align(Alignment.Center) .align(Alignment.Center),
.semantics {
this.contentDescription = ""
},
color = MaterialTheme.colorScheme.onTertiary, 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.foundation.Image
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -6,21 +6,25 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.layout.ContentScale 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 import androidx.compose.ui.tooling.preview.Preview
@Composable @Composable
@Preview(widthDp = 40, heightDp = 40) @Preview(widthDp = 40, heightDp = 40)
fun AvatarFromImageBitmap( fun AvatarImage(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
bitmap: ImageBitmap? = null, bitmap: ImageBitmap? = null,
fallbackText: String = "?", fallbackText: String? = null,
contentDescription: String = "", contentDescription: String = "",
) { ) {
if (bitmap == null) { if (bitmap == null) {
AvatarFromDefault( AvatarEmpty(
modifier = modifier, modifier = modifier
fallbackText = fallbackText, .semantics {
contentDescription = contentDescription, this.contentDescription = contentDescription
},
text = fallbackText,
) )
} else { } else {
Image( Image(

View file

@ -15,7 +15,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import eu.steffo.twom.matrix.avatar.AvatarFromImageBitmap
import eu.steffo.twom.utils.BitmapUtilities import eu.steffo.twom.utils.BitmapUtilities
@Composable @Composable
@ -49,7 +48,7 @@ fun AvatarPicker(
launcher.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)) launcher.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly))
} }
) { ) {
AvatarFromImageBitmap( AvatarImage(
bitmap = selection?.asImageBitmap(), bitmap = selection?.asImageBitmap(),
fallbackText = fallbackText, 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.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
@ -12,16 +12,15 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import eu.steffo.twom.matrix.LocalSession import eu.steffo.twom.composables.matrix.LocalSession
import org.matrix.android.sdk.api.failure.Failure
import java.io.File import java.io.File
@Composable @Composable
@Preview(widthDp = 40, heightDp = 40) @Preview(widthDp = 40, heightDp = 40)
fun AvatarFromURL( fun AvatarURL(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
url: String? = "", url: String? = "",
fallbackText: String = "?", fallbackText: String? = null,
contentDescription: String = "", contentDescription: String = "",
) { ) {
val session = LocalSession.current val session = LocalSession.current
@ -29,22 +28,22 @@ fun AvatarFromURL(
LaunchedEffect(session, url) GetAvatar@{ LaunchedEffect(session, url) GetAvatar@{
if (session == null) { if (session == null) {
Log.d("Avatar", "Not doing anything, session is null.") Log.d("AvatarURL", "Not doing anything, session is null.")
bitmap = null bitmap = null
return@GetAvatar return@GetAvatar
} }
if (url == null) { if (url == null) {
Log.d("Avatar", "URL is null, not downloading anything.") Log.d("AvatarURL", "URL is null, not downloading anything.")
bitmap = null bitmap = null
return@GetAvatar return@GetAvatar
} }
if (url.isEmpty()) { 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 bitmap = null
return@GetAvatar return@GetAvatar
} }
Log.d("Avatar", "Downloading avatar at: $url") Log.d("AvatarURL", "Downloading avatar at: $url")
lateinit var avatarFile: File lateinit var avatarFile: File
try { try {
avatarFile = session.fileService().downloadFile( avatarFile = session.fileService().downloadFile(
@ -53,27 +52,20 @@ fun AvatarFromURL(
mimeType = null, mimeType = null,
elementToDecrypt = null, elementToDecrypt = null,
) )
} catch (f: Failure.OtherServerError) { } catch (e: Throwable) {
Log.e("Avatar", "Unable to download avatar at: $url", f) Log.e("AvatarURL", "Unable to download avatar at: $url", e)
return@GetAvatar return@GetAvatar
} }
// TODO: Should I check the MIME type? And the size of the image? // 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) bitmap = BitmapFactory.decodeFile(avatarFile.absolutePath)
} }
if (bitmap == null) { AvatarImage(
AvatarFromDefault( modifier = modifier,
modifier = modifier, bitmap = bitmap?.asImageBitmap(),
fallbackText = fallbackText, fallbackText = fallbackText,
contentDescription = contentDescription contentDescription = contentDescription,
) )
} else {
AvatarFromImageBitmap(
modifier = modifier,
bitmap = bitmap!!.asImageBitmap(),
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 android.util.Log
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -9,11 +9,11 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import eu.steffo.twom.matrix.LocalSession import eu.steffo.twom.composables.matrix.LocalSession
@Composable @Composable
@Preview(widthDp = 40, heightDp = 40) @Preview(widthDp = 40, heightDp = 40)
fun AvatarFromUserId( fun AvatarUserId(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
userId: String = "", userId: String = "",
fallbackText: String = "?", fallbackText: String = "?",
@ -24,30 +24,24 @@ fun AvatarFromUserId(
LaunchedEffect(session, userId) GetAvatarUrl@{ LaunchedEffect(session, userId) GetAvatarUrl@{
if (session == null) { if (session == null) {
Log.d("UserAvatar", "Not doing anything, session is null.") Log.d("AvatarUser", "Not doing anything, session is null.")
return@GetAvatarUrl return@GetAvatarUrl
} }
if (userId.isEmpty()) { if (userId.isEmpty()) {
Log.d("UserAvatar", "Not doing anything, userId is empty.") Log.d("AvatarUser", "Not doing anything, userId is empty.")
return@GetAvatarUrl return@GetAvatarUrl
} }
Log.d("UserAvatar", "Retrieving avatar url for: $userId...")
Log.d("AvatarUser", "Retrieving avatar url for: $userId...")
avatarUrl = session.profileService().getAvatarUrl(userId).getOrNull() 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) { AvatarURL(
AvatarFromDefault( modifier = modifier,
modifier = modifier, url = avatarUrl,
fallbackText = fallbackText, fallbackText = fallbackText,
contentDescription = contentDescription, contentDescription = contentDescription,
) )
} else {
AvatarFromURL(
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 androidx.compose.ui.unit.dp
import eu.steffo.twom.R import eu.steffo.twom.R
import eu.steffo.twom.composables.avatar.AvatarPicker 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 import eu.steffo.twom.utils.BitmapUtilities
@Composable @Composable
@ -39,7 +39,7 @@ fun CreateRoomForm(
var avatarUri by rememberSaveable { mutableStateOf<Uri?>(null) } var avatarUri by rememberSaveable { mutableStateOf<Uri?>(null) }
Column(modifier) { Column(modifier) {
Row(TwoMPadding.base) { Row(Modifier.basePadding()) {
val avatarContentDescription = stringResource(R.string.create_avatar_label) val avatarContentDescription = stringResource(R.string.create_avatar_label)
AvatarPicker( AvatarPicker(
modifier = Modifier modifier = Modifier
@ -67,7 +67,7 @@ fun CreateRoomForm(
) )
} }
Row(TwoMPadding.base) { Row(Modifier.basePadding()) {
TextField( TextField(
modifier = Modifier modifier = Modifier
.height(180.dp) .height(180.dp)
@ -80,7 +80,7 @@ fun CreateRoomForm(
) )
} }
Row(TwoMPadding.base) { Row(Modifier.basePadding()) {
Button( Button(
modifier = Modifier modifier = Modifier
.fillMaxWidth(), .fillMaxWidth(),

View file

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

View file

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

View file

@ -1,5 +1,7 @@
package eu.steffo.twom.composables.main 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.Icons
import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Add
import androidx.compose.material3.ExtendedFloatingActionButton import androidx.compose.material3.ExtendedFloatingActionButton
@ -10,16 +12,24 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import eu.steffo.twom.R import eu.steffo.twom.R
import eu.steffo.twom.activities.CreateRoomActivity
@Composable @Composable
@Preview @Preview
fun CreateRoomFAB( fun CreateRoomFAB(
modifier: Modifier = Modifier, 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( ExtendedFloatingActionButton(
modifier = modifier, modifier = modifier,
onClick = onClick, onClick = { launcher.launch() },
icon = { icon = {
Icon( Icon(
Icons.Filled.Add, Icons.Filled.Add,

View file

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

View file

@ -8,7 +8,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import eu.steffo.twom.R import eu.steffo.twom.R
import eu.steffo.twom.theme.TwoMPadding import eu.steffo.twom.composables.theme.basePadding
@Composable @Composable
@Preview(showBackground = true) @Preview(showBackground = true)
@ -17,10 +17,10 @@ fun MainContentNotLoggedIn(
onClickLogin: () -> Unit = {}, onClickLogin: () -> Unit = {},
) { ) {
Column(modifier) { Column(modifier) {
Row(TwoMPadding.base) { Row(Modifier.basePadding()) {
Text(LocalContext.current.getString(R.string.main_notloggedin_text_1)) 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)) 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.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import eu.steffo.twom.matrix.LocalSession import eu.steffo.twom.composables.matrix.LocalSession
import eu.steffo.twom.theme.TwoMTheme import eu.steffo.twom.composables.theme.TwoMTheme
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
@Composable @Composable
@ -15,6 +15,7 @@ import org.matrix.android.sdk.api.session.Session
fun MainScaffold( fun MainScaffold(
processLogin: () -> Unit = {}, processLogin: () -> Unit = {},
processLogout: () -> Unit = {}, processLogout: () -> Unit = {},
processCreate: (name: String, description: String, avatarUri: String?) -> Unit = { _, _, _ -> },
session: Session? = null, session: Session? = null,
) { ) {
TwoMTheme { TwoMTheme {
@ -27,7 +28,9 @@ fun MainScaffold(
) )
}, },
floatingActionButton = { floatingActionButton = {
CreateRoomFAB() CreateRoomFAB(
onCreateParamsSelected = processCreate,
)
}, },
content = { content = {
if (session == null) { 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 android.util.Log
import androidx.activity.compose.rememberLauncherForActivityResult 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.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import eu.steffo.twom.R 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.composables.errorhandling.LocalizableError
import eu.steffo.twom.matrix.LocalSession import eu.steffo.twom.composables.matrix.LocalSession
import eu.steffo.twom.matrix.avatar.AvatarFromURL
import eu.steffo.twom.theme.ErrorText
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary
@ -50,11 +50,11 @@ fun RoomListItem(
var expanded by rememberSaveable { mutableStateOf(false) } var expanded by rememberSaveable { mutableStateOf(false) }
val error by remember { mutableStateOf(LocalizableError()) } val error by remember { mutableStateOf(LocalizableError()) }
val roomActivityLauncher = rememberLauncherForActivityResult(RoomActivity.Contract()) {} val viewRoomActivityLauncher = rememberLauncherForActivityResult(ViewRoomActivity.Contract()) {}
fun openRoom() { fun openRoom() {
Log.i("Main", "Opening room `$roomId`...") Log.i("Main", "Opening room `$roomId`...")
roomActivityLauncher.launch(roomId) viewRoomActivityLauncher.launch(roomId)
} }
suspend fun leaveRoom() { suspend fun leaveRoom() {
@ -83,7 +83,7 @@ fun RoomListItem(
.size(40.dp) .size(40.dp)
.clip(MaterialTheme.shapes.medium) .clip(MaterialTheme.shapes.medium)
) { ) {
AvatarFromURL( AvatarURL(
// FIXME: URL can appearently be set before the image is available on the homeserver // FIXME: URL can appearently be set before the image is available on the homeserver
url = roomSummary.avatarUrl, 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 androidx.compose.runtime.staticCompositionLocalOf
import org.matrix.android.sdk.api.session.Session 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 android.app.Activity
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
@ -18,6 +18,7 @@ import androidx.core.view.WindowCompat
fun TwoMTheme( fun TwoMTheme(
content: @Composable () -> Unit content: @Composable () -> Unit
) { ) {
val view = LocalView.current
val context = LocalContext.current val context = LocalContext.current
val darkTheme = isSystemInDarkTheme() val darkTheme = isSystemInDarkTheme()
@ -27,21 +28,21 @@ fun TwoMTheme(
} }
val typography = Typography() val typography = Typography()
MaterialTheme(
colorScheme = colorScheme,
typography = typography,
content = content
)
val view = LocalView.current
if (!view.isInEditMode) { if (!view.isInEditMode) {
SideEffect { SideEffect {
val window = (view.context as Activity).window val window = (context as Activity).window
window.statusBarColor = colorScheme.surface.toArgb() window.statusBarColor = colorScheme.surface.toArgb()
window.navigationBarColor = colorScheme.surface.toArgb() window.navigationBarColor = colorScheme.surface.toArgb()
val insets = WindowCompat.getInsetsController(window, view) val insets = WindowCompat.getInsetsController(window, view)
insets.isAppearanceLightStatusBars = !darkTheme insets.isAppearanceLightStatusBars = !darkTheme
insets.isAppearanceLightNavigationBars = !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 androidx.compose.runtime.staticCompositionLocalOf
import org.matrix.android.sdk.api.session.room.Room 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 androidx.compose.runtime.staticCompositionLocalOf
import org.matrix.android.sdk.api.session.room.model.RoomSummary 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.ExperimentalMaterial3Api
import androidx.compose.material3.FilterChip import androidx.compose.material3.FilterChip
@ -8,47 +8,45 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier 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 androidx.compose.ui.unit.dp
import eu.steffo.twom.utils.RSVPAnswer
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun RSVPAnswerFilterChip( @Preview
fun RSVPChip(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
representing: RSVPAnswer,
selected: Boolean = false, selected: Boolean = false,
onClick: () -> Unit = {}, onClick: () -> Unit = {},
representedAnswer: RSVPAnswer = RSVPAnswer.UNKNOWN,
) { ) {
val icon = representing.toIcon()
val colorRole = representing.toStaticColorRole()
val labelResourceId = representing.toLabelResourceId()
FilterChip( FilterChip(
modifier = modifier, modifier = modifier,
selected = selected, selected = selected,
onClick = onClick, onClick = onClick,
leadingIcon = { leadingIcon = {
Icon( Icon(
imageVector = icon, imageVector = representedAnswer.icon,
contentDescription = null, contentDescription = null,
) )
}, },
label = { label = {
Text( Text(
text = stringResource(labelResourceId), text = representedAnswer.toLabel() ?: "[missing label]",
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,
) )
}, },
colors = FilterChipDefaults.filterChipColors( colors = FilterChipDefaults.filterChipColors(
iconColor = colorRole.value, iconColor = representedAnswer.staticColorRole.color(),
labelColor = colorRole.value, labelColor = representedAnswer.staticColorRole.color(),
selectedContainerColor = colorRole.valueContainer, selectedContainerColor = representedAnswer.staticColorRole.containerColor(),
selectedLeadingIconColor = colorRole.onValueContainer, selectedLeadingIconColor = representedAnswer.staticColorRole.onContainerColor(),
selectedLabelColor = colorRole.onValueContainer, selectedLabelColor = representedAnswer.staticColorRole.onContainerColor(),
), ),
border = FilterChipDefaults.filterChipBorder( border = FilterChipDefaults.filterChipBorder(
borderColor = MaterialTheme.colorScheme.surfaceVariant, borderColor = MaterialTheme.colorScheme.surfaceVariant,
selectedBorderColor = colorRole.onValueContainer, selectedBorderColor = representedAnswer.staticColorRole.onContainerColor(),
borderWidth = 1.dp, borderWidth = 1.dp,
selectedBorderWidth = 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.horizontalScroll
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@ -9,11 +9,12 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp 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 @Composable
@Preview @Preview
fun RSVPAnswerSelectRow( fun RSVPChipRow(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
value: RSVPAnswer = RSVPAnswer.UNKNOWN, value: RSVPAnswer = RSVPAnswer.UNKNOWN,
onChange: (answer: RSVPAnswer) -> Unit = {}, onChange: (answer: RSVPAnswer) -> Unit = {},
@ -37,27 +38,27 @@ fun RSVPAnswerSelectRow(
modifier = Modifier modifier = Modifier
.padding(start = 8.dp, end = 8.dp) .padding(start = 8.dp, end = 8.dp)
) { ) {
RSVPAnswerFilterChip( RSVPChip(
modifier = TwoMPadding.chips, modifier = Modifier.chipPadding(),
representing = RSVPAnswer.SURE, representedAnswer = RSVPAnswer.SURE,
selected = (value == RSVPAnswer.SURE), selected = (value == RSVPAnswer.SURE),
onClick = toggleSwitch(RSVPAnswer.SURE) onClick = toggleSwitch(RSVPAnswer.SURE)
) )
RSVPAnswerFilterChip( RSVPChip(
modifier = TwoMPadding.chips, modifier = Modifier.chipPadding(),
representing = RSVPAnswer.LATER, representedAnswer = RSVPAnswer.LATER,
selected = (value == RSVPAnswer.LATER), selected = (value == RSVPAnswer.LATER),
onClick = toggleSwitch(RSVPAnswer.LATER) onClick = toggleSwitch(RSVPAnswer.LATER)
) )
RSVPAnswerFilterChip( RSVPChip(
modifier = TwoMPadding.chips, modifier = Modifier.chipPadding(),
representing = RSVPAnswer.MAYBE, representedAnswer = RSVPAnswer.MAYBE,
selected = (value == RSVPAnswer.MAYBE), selected = (value == RSVPAnswer.MAYBE),
onClick = toggleSwitch(RSVPAnswer.MAYBE) onClick = toggleSwitch(RSVPAnswer.MAYBE)
) )
RSVPAnswerFilterChip( RSVPChip(
modifier = TwoMPadding.chips, modifier = Modifier.chipPadding(),
representing = RSVPAnswer.NOWAY, representedAnswer = RSVPAnswer.NOWAY,
selected = (value == RSVPAnswer.NOWAY), selected = (value == RSVPAnswer.NOWAY),
onClick = toggleSwitch(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.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
@ -9,6 +9,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import eu.steffo.twom.R import eu.steffo.twom.R
import eu.steffo.twom.utils.RSVPAnswer
@Composable @Composable
@Preview @Preview
@ -16,22 +17,20 @@ fun RSVPUpdateButton(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
enabled: Boolean = true, enabled: Boolean = true,
onClick: () -> Unit = {}, onClick: () -> Unit = {},
rsvpAnswer: RSVPAnswer = RSVPAnswer.UNKNOWN, currentAnswer: RSVPAnswer = RSVPAnswer.UNKNOWN,
) { ) {
val colorRole = rsvpAnswer.toStaticColorRole()
Button( Button(
modifier = modifier, modifier = modifier,
enabled = enabled, enabled = enabled,
onClick = onClick, onClick = onClick,
shape = MaterialTheme.shapes.small, shape = MaterialTheme.shapes.small,
colors = ButtonDefaults.buttonColors( colors = ButtonDefaults.buttonColors(
containerColor = colorRole.value, containerColor = currentAnswer.staticColorRole.color(),
contentColor = colorRole.onValue, contentColor = currentAnswer.staticColorRole.onColor(),
) )
) { ) {
Text( 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.foundation.layout.padding
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
@ -7,16 +7,15 @@ import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import eu.steffo.twom.matrix.LocalSession import eu.steffo.twom.composables.matrix.LocalSession
import eu.steffo.twom.theme.TwoMTheme import eu.steffo.twom.composables.theme.TwoMTheme
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import java.util.Optional import java.util.Optional
@Composable @Composable
fun RoomActivityScaffold( fun ViewRoomScaffold(
session: Session, session: Session,
roomId: String, roomId: String,
onBack: () -> Unit = {},
) { ) {
val room = Optional.ofNullable(session.roomService().getRoom(roomId)) val room = Optional.ofNullable(session.roomService().getRoom(roomId))
val roomSummary by session.roomService().getRoomSummaryLive(roomId).observeAsState() val roomSummary by session.roomService().getRoomSummaryLive(roomId).observeAsState()
@ -27,12 +26,10 @@ fun RoomActivityScaffold(
CompositionLocalProvider(LocalRoomSummary provides roomSummary) { CompositionLocalProvider(LocalRoomSummary provides roomSummary) {
Scaffold( Scaffold(
topBar = { topBar = {
RoomActivityTopBar( ViewRoomTopBar()
onBack = onBack,
)
}, },
content = { content = {
RoomActivityContent( ViewRoomContent(
modifier = Modifier.padding(it), 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.content.Context
import android.util.Log import android.util.Log
@ -9,7 +9,7 @@ import org.matrix.android.sdk.api.MatrixConfiguration
/** /**
* Object containing the global state of the application. * Object containing the global state of the application.
*/ */
object TwoMMatrix { object TwoMGlobals {
/** /**
* The global [Matrix] object of the application. * The global [Matrix] object of the application.
* *
@ -38,4 +38,10 @@ object TwoMMatrix {
} }
const val ROOM_TYPE = "eu.steffo.twom.happening" 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 android.content.Context
import eu.steffo.twom.R 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_later_placeholder">Why will you be late?</string>
<string name="room_rsvp_maybe_placeholder">What will determine your partecipation?</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_noway_placeholder">Why won\'t you partecipate?</string>
<string name="room_rsvp_unknown_response">Hasn\'t answered yet</string> <string name="room_rsvp_update_label">Update</string>
<string name="room_rsvp_unknown_placeholder">Leave a comment…</string>
<string name="room_update_label">Update</string>
<string name="error_session_missing">The Matrix session context has not been initialized.</string> <string name="error_session_missing">The Matrix session context has not been initialized.</string>
<string name="room_error_room_notfound">Could not find the requested Matrix room.</string> <string name="room_error_room_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_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_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_error_roomsummary_notfound">Could not find the requested Matrix room summary.</string>
<string name="room_invite_username_placeholder">\@steffotwo:candy.steffo.eu</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_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="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="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> </resources>

View file

@ -3,6 +3,6 @@
<style name="Theme.TwoM" parent="@style/Theme.Material3.DayNight" /> <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> </resources>