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

Add support for sending invites

This commit is contained in:
Steffo 2024-01-20 13:29:55 +01:00
parent 822a2e5770
commit 23de2ed278
Signed by: steffo
GPG key ID: 5ADA3868646C3FC0
21 changed files with 271 additions and 178 deletions

View file

@ -7,7 +7,7 @@ import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContract
import eu.steffo.twom.composables.configureroom.CreateRoomScaffold
import eu.steffo.twom.composables.configureroom.ConfigureRoomScaffold
class ConfigureRoomActivity : ComponentActivity() {
@ -51,6 +51,6 @@ class ConfigureRoomActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent { CreateRoomScaffold() }
setContent { ConfigureRoomScaffold() }
}
}

View file

@ -1,35 +0,0 @@
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, ConfigureRoomActivity::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

@ -17,16 +17,17 @@ import androidx.compose.ui.tooling.preview.Preview
fun AvatarEmpty(
modifier: Modifier = Modifier,
text: String? = null,
alpha: Float = 1.0f,
) {
Box(
modifier = modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.tertiary),
.background(MaterialTheme.colorScheme.tertiary.copy(alpha = alpha)),
) {
Text(
modifier = Modifier
.align(Alignment.Center),
color = MaterialTheme.colorScheme.onTertiary,
color = MaterialTheme.colorScheme.onTertiary.copy(alpha = alpha),
text = text ?: "?",
)
}

View file

@ -17,6 +17,7 @@ fun AvatarImage(
bitmap: ImageBitmap? = null,
fallbackText: String? = null,
contentDescription: String = "",
alpha: Float = 1.0f,
) {
if (bitmap == null) {
AvatarEmpty(
@ -25,6 +26,7 @@ fun AvatarImage(
this.contentDescription = contentDescription
},
text = fallbackText,
alpha = alpha,
)
} else {
Image(
@ -33,6 +35,7 @@ fun AvatarImage(
contentDescription = contentDescription,
contentScale = ContentScale.Crop,
alignment = Alignment.Center,
alpha = alpha,
)
}
}

View file

@ -23,6 +23,7 @@ fun AvatarPicker(
modifier: Modifier = Modifier,
fallbackText: String = "?",
onPick: (bitmap: Bitmap) -> Unit = {},
alpha: Float = 1.0f,
) {
val context = LocalContext.current
val resolver = context.contentResolver
@ -51,6 +52,7 @@ fun AvatarPicker(
AvatarImage(
bitmap = selection?.asImageBitmap(),
fallbackText = fallbackText,
alpha = alpha,
)
}
}

View file

@ -22,6 +22,7 @@ fun AvatarURL(
url: String? = "",
fallbackText: String? = null,
contentDescription: String = "",
alpha: Float = 1.0f,
) {
val session = LocalSession.current
var bitmap by remember { mutableStateOf<Bitmap?>(null) }
@ -67,5 +68,6 @@ fun AvatarURL(
bitmap = bitmap?.asImageBitmap(),
fallbackText = fallbackText,
contentDescription = contentDescription,
alpha = alpha,
)
}

View file

@ -13,11 +13,13 @@ fun AvatarUser(
user: User? = null,
fallbackText: String? = null,
contentDescription: String = "",
alpha: Float = 1.0f,
) {
AvatarURL(
modifier = modifier,
url = user?.avatarUrl,
fallbackText = user?.toMatrixItem()?.firstLetterOfDisplayName(),
contentDescription = contentDescription,
alpha = alpha,
)
}

View file

@ -18,6 +18,7 @@ fun AvatarUserId(
userId: String = "",
fallbackText: String = "?",
contentDescription: String = "",
alpha: Float = 1.0f,
) {
val session = LocalSession.current
var avatarUrl by rememberSaveable { mutableStateOf<String?>(null) }
@ -43,5 +44,6 @@ fun AvatarUserId(
url = avatarUrl,
fallbackText = fallbackText,
contentDescription = contentDescription,
alpha = alpha,
)
}

View file

@ -32,7 +32,7 @@ import eu.steffo.twom.utils.BitmapUtilities
@Preview(showBackground = true)
fun ConfigureRoomForm(
modifier: Modifier = Modifier,
onSubmit: (name: String, description: String, avatarUri: Uri?) -> Unit = { _, _, _ -> },
onSubmit: (name: String, description: String, avatarUri: String?) -> Unit = { _, _, _ -> },
) {
var name by rememberSaveable { mutableStateOf("") }
var description by rememberSaveable { mutableStateOf("") }
@ -85,7 +85,7 @@ fun ConfigureRoomForm(
modifier = Modifier
.fillMaxWidth(),
onClick = {
onSubmit(name, description, avatarUri)
onSubmit(name, description, avatarUri.toString())
},
) {
Text(stringResource(R.string.create_complete_text))

View file

@ -2,7 +2,6 @@ package eu.steffo.twom.composables.configureroom
import android.app.Activity
import android.content.Intent
import android.net.Uri
import androidx.activity.ComponentActivity
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
@ -15,11 +14,11 @@ import eu.steffo.twom.composables.theme.TwoMTheme
@Composable
@Preview
fun CreateRoomScaffold() {
fun ConfigureRoomScaffold() {
val context = LocalContext.current
val activity = context as Activity
fun submitActivity(name: String, description: String, avatarUri: Uri?) {
fun submitActivity(name: String, description: String, avatarUri: String?) {
val resultIntent = Intent()
resultIntent.putExtra(ConfigureRoomActivity.NAME_EXTRA, name)
resultIntent.putExtra(ConfigureRoomActivity.DESCRIPTION_EXTRA, description)
@ -34,12 +33,12 @@ fun CreateRoomScaffold() {
TwoMTheme {
Scaffold(
topBar = {
CreateActivityTopBar()
ConfigureActivityTopBar()
},
content = {
ConfigureRoomForm(
modifier = Modifier.padding(it),
onSubmit = { name: String, description: String, avatarUri: Uri? ->
onSubmit = { name: String, description: String, avatarUri: String? ->
submitActivity(name, description, avatarUri)
}
)

View file

@ -13,7 +13,7 @@ import eu.steffo.twom.composables.navigation.BackIconButton
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@Preview
fun CreateActivityTopBar(
fun ConfigureActivityTopBar(
modifier: Modifier = Modifier,
) {
TopAppBar(

View file

@ -1,47 +0,0 @@
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

@ -1,54 +0,0 @@
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

@ -1,9 +1,7 @@
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.material.icons.filled.Email
import androidx.compose.material3.ExtendedFloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
@ -12,27 +10,20 @@ 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,
onClick: () -> Unit = {},
onUserSelected: (userId: String) -> Unit = {},
) {
val launcher =
rememberLauncherForActivityResult(InviteUserActivity.Contract()) {
if (it != null) {
onUserSelected(it)
}
}
ExtendedFloatingActionButton(
modifier = modifier,
onClick = { launcher.launch() },
onClick = { onClick() },
icon = {
Icon(
Icons.Filled.Add,
Icons.Filled.Email,
contentDescription = null
)
},

View file

@ -0,0 +1,72 @@
package eu.steffo.twom.composables.viewroom
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.SheetState
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import eu.steffo.twom.R
import eu.steffo.twom.composables.theme.basePadding
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun InviteSheet(
sheetState: SheetState,
modifier: Modifier = Modifier,
onDismissed: () -> Unit = {},
onCompleted: () -> Unit = {},
) {
val scope = rememberCoroutineScope()
ModalBottomSheet(
modifier = modifier,
sheetState = sheetState,
onDismissRequest = {
// Not super sure what this is for
// https://developer.android.com/jetpack/compose/components/bottom-sheets
scope.launch {
sheetState.hide()
}.invokeOnCompletion {
if (!sheetState.isVisible) {
onDismissed()
}
}
},
) {
// Hack required as it seems that ModalBottomSheet does not take in account screen insets yet
Column(Modifier.padding(bottom = 80.dp)) {
Text(
modifier = Modifier
.basePadding()
.fillMaxWidth(),
text = stringResource(R.string.room_invite_title),
style = MaterialTheme.typography.headlineSmall,
textAlign = TextAlign.Center,
)
InviteUserForm(
onDone = {
scope.launch {
sheetState.hide()
}.invokeOnCompletion {
if (!sheetState.isVisible) {
onCompleted()
}
}
}
)
}
}
}

View file

@ -0,0 +1,114 @@
package eu.steffo.twom.composables.viewroom
import android.util.Log
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import eu.steffo.twom.R
import eu.steffo.twom.composables.errorhandling.ErrorText
import eu.steffo.twom.composables.errorhandling.LoadingText
import eu.steffo.twom.composables.errorhandling.LocalizableError
import eu.steffo.twom.composables.theme.basePadding
import kotlinx.coroutines.launch
import kotlin.jvm.optionals.getOrNull
@Composable
fun InviteUserForm(
onDone: () -> Unit = {},
) {
val scope = rememberCoroutineScope()
var userId by rememberSaveable { mutableStateOf("") }
val roomRequest = LocalRoom.current
if (roomRequest == null) {
LoadingText(
modifier = Modifier.basePadding(),
)
return
}
val room = roomRequest.getOrNull()
if (room == null) {
ErrorText(
modifier = Modifier.basePadding(),
text = stringResource(R.string.room_error_room_notfound)
)
return
}
TextField(
modifier = Modifier
.basePadding()
.fillMaxWidth(),
value = userId,
onValueChange = { userId = it },
singleLine = true,
label = {
Text(
text = stringResource(R.string.room_invite_username_label)
)
},
placeholder = {
Text(
text = stringResource(R.string.room_invite_username_placeholder)
)
},
)
var busy by rememberSaveable { mutableStateOf(false) }
val error by remember { mutableStateOf(LocalizableError()) }
Button(
modifier = Modifier
.basePadding()
.fillMaxWidth(),
// FIXME: Maybe I should validate usernames with a regex
enabled = (!busy && userId.contains("@") && userId.contains(":")),
onClick = {
scope.launch SendInvite@{
busy = true
error.clear()
Log.d("Room", "Sending invite to `$userId`...")
try {
room.membershipService().invite(userId)
} catch (e: Throwable) {
Log.e("Room", "Failed to send invite to `$userId`: $error")
error.set(R.string.room_error_invite_generic, e)
busy = false
return@SendInvite
}
Log.d("Room", "Successfully sent invite to `$userId`!")
onDone()
busy = false
}
},
) {
Text(
text = stringResource(R.string.room_invite_button_label)
)
}
error.Show {
ErrorText(
modifier = Modifier
.basePadding()
.fillMaxWidth(),
text = it,
)
}
}

View file

@ -27,6 +27,7 @@ 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 eu.steffo.twom.utils.RSVPAnswer
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
@ -72,18 +73,24 @@ fun MemberListItem(
Log.d("UserListItem", "Resolved user: $memberId")
}
val rsvp = observeRSVP(room = room, member = member)
val rsvp = observeRSVP(room = room, member = member) ?: return
var expanded by rememberSaveable { mutableStateOf(false) }
val alpha = if (rsvp.answer == RSVPAnswer.PENDING) 0.4f else 1.0f
val color = rsvp.answer.staticColorRole.color().copy(alpha)
ListItem(
modifier = modifier.combinedClickable(
onClick = {},
onLongClick = { expanded = true },
),
modifier = modifier
.combinedClickable(
onClick = {},
onLongClick = { expanded = true },
),
headlineContent = {
Text(
text = user?.displayName ?: stringResource(R.string.user_unresolved_name),
color = color,
style = MaterialTheme.typography.titleMedium,
)
},
leadingContent = {
@ -95,6 +102,7 @@ fun MemberListItem(
) {
AvatarUser(
user = user,
alpha = alpha,
)
}
},
@ -102,14 +110,14 @@ fun MemberListItem(
Icon(
imageVector = rsvp.answer.icon,
contentDescription = rsvp.answer.toResponse(),
tint = rsvp.answer.staticColorRole.color(),
tint = color,
)
},
supportingContent = {
if (rsvp.comment != "") {
Text(
text = rsvp.comment,
color = rsvp.answer.staticColorRole.color(),
color = color,
)
}
},
@ -119,11 +127,13 @@ fun MemberListItem(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
DropdownMenuItem(
text = {
Text(stringResource(R.string.room_uninvite_label))
},
onClick = { expanded = false }
)
if (member.userId != session.myUserId) {
DropdownMenuItem(
text = {
Text(stringResource(R.string.room_uninvite_label))
},
onClick = { expanded = false },
)
}
}
}

View file

@ -20,6 +20,7 @@ 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 org.matrix.android.sdk.api.session.room.model.Membership
import kotlin.jvm.optionals.getOrNull
@Composable
@ -62,7 +63,7 @@ fun ViewRoomForm() {
}
val member = room.membershipService().getRoomMember(session.myUserId)
if (member == null) {
if (member == null || member.membership != Membership.JOIN) {
Row(Modifier.basePadding()) {
ErrorText(
text = stringResource(R.string.room_error_self_notfound)
@ -71,7 +72,8 @@ fun ViewRoomForm() {
return
}
val published = observeRSVP(room = room, member = member)
// JOIN status is checked above
val published = observeRSVP(room = room, member = member)!!
var isPublishRunning by rememberSaveable { mutableStateOf(false) }
val publishError by remember { mutableStateOf(LocalizableError()) }

View file

@ -1,17 +1,25 @@
package eu.steffo.twom.composables.viewroom
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Scaffold
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import eu.steffo.twom.composables.matrix.LocalSession
import eu.steffo.twom.composables.theme.TwoMTheme
import org.matrix.android.sdk.api.session.Session
import java.util.Optional
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
@Composable
fun ViewRoomScaffold(
session: Session,
@ -20,6 +28,9 @@ fun ViewRoomScaffold(
val room = Optional.ofNullable(session.roomService().getRoom(roomId))
val roomSummary by session.roomService().getRoomSummaryLive(roomId).observeAsState()
val inviteDialogState = rememberModalBottomSheetState()
var inviteDialogExpanded by rememberSaveable { mutableStateOf(false) }
TwoMTheme {
CompositionLocalProvider(LocalSession provides session) {
CompositionLocalProvider(LocalRoom provides room) {
@ -32,7 +43,19 @@ fun ViewRoomScaffold(
ViewRoomContent(
modifier = Modifier.padding(it),
)
if (inviteDialogExpanded) {
InviteSheet(
// FIXME: Does this work?
modifier = Modifier.consumeWindowInsets(it),
sheetState = inviteDialogState,
onDismissed = { inviteDialogExpanded = false },
onCompleted = { inviteDialogExpanded = false },
)
}
},
floatingActionButton = {
InviteFAB(onClick = { inviteDialogExpanded = true })
}
)
}
}

View file

@ -12,7 +12,7 @@ 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 {
fun observeRSVP(room: Room, member: RoomMemberSummary): RSVP? {
if (member.membership == Membership.INVITE) {
return RSVP(
event = null,
@ -21,6 +21,11 @@ fun observeRSVP(room: Room, member: RoomMemberSummary): RSVP {
)
}
// TODO: Add a DECLINED variant?
if (member.membership == Membership.LEAVE || member.membership == Membership.BAN) {
return null
}
val request by room.stateService().getStateEventLive(
eventType = TwoMGlobals.RSVP_STATE_TYPE,
stateKey = QueryStringValue.Equals(member.userId),

View file

@ -59,7 +59,7 @@
<string name="room_error_roomsummary_missing">The Matrix room summary context has not been initialized.</string>
<string name="room_error_roomsummary_notfound">Could not find the requested Matrix room summary.</string>
<string name="room_invite_username_placeholder">\@steffotwo:candy.steffo.eu</string>
<string name="room_invite_username_label">Username</string>
<string name="room_invite_username_label">User you want to invite</string>
<string name="room_rsvp_comment_label">Reason</string>
<string name="room_invite_button_label">Invite</string>
<string name="room_invite_title">Send an invite</string>
@ -83,4 +83,5 @@
<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>
<string name="room_error_self_notfound">You have been removed from the room.</string>
<string name="room_error_invite_generic">Something went wrong while sending the invite: %1$s</string>
</resources>