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

It works!

This commit is contained in:
Steffo 2024-01-09 04:27:50 +01:00
parent a6e562c14b
commit acf9f34f59
Signed by: steffo
GPG key ID: 2A24051445686895
10 changed files with 284 additions and 115 deletions

View file

@ -19,6 +19,7 @@ import kotlinx.coroutines.launch
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.CreateRoomPreset
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomStateEvent
class MainActivity : ComponentActivity() {
@ -176,6 +177,52 @@ class MainActivity : ComponentActivity() {
createRoomParams.topic = description
createRoomParams.preset = CreateRoomPreset.PRESET_PRIVATE_CHAT
createRoomParams.roomType = TwoMMatrix.ROOM_TYPE
createRoomParams.initialStates = mutableListOf(
CreateRoomStateEvent(
type = "m.room.power_levels",
content = mapOf(
// Users start with a power level of 0
"users_default" to 0,
// Allow only the party creator to send arbitrary events
"events_default" to 100,
// Allow only the party creator to send arbitrary states
"state_default" to 100,
// Allow only party officers to send invites
"invite" to 50,
// Allow only party officers to kick invitees
"kick" to 50,
// Allow only party officers to ban invitees
"ban" to 50,
// Allow only party officers to redact other people's events
"redact" to 50,
"notifications" to mapOf(
// Allow only party officers to ping the room
"room" to 50,
),
"events" to mapOf(
// Allow party officers to rename the room
"m.room.name" to 50,
// Allow party officers to change the room avatar
"m.room.avatar" to 50,
// Allow party officers to change the room topic
"m.room.topic" to 50,
// Allow everyone to redact their own states
"m.room.redaction" to 0,
// Allow everyone to set RSVPs
// FIXME: Do we really want everyone to set RSVPs? Maybe we should use m.room.member instead?
"eu.steffo.twom.rsvp" to 0,
),
"users" to mapOf(
// Give ourselves admin permissions
session!!.myUserId to 100,
)
)
)
)
when (avatarUri?.toFile()?.isFile) {
false -> {

View file

@ -25,8 +25,8 @@ fun MemberListItem(
modifier: Modifier = Modifier,
memberId: String,
onClickMember: (memberId: String) -> Unit = {},
rsvpAnswer: RSVPAnswer? = null,
rsvpComment: String = "",
rsvpAnswer: RSVPAnswer,
rsvpComment: String,
) {
val session = LocalSession.current

View file

@ -1,6 +1,8 @@
package eu.steffo.twom.room
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
@ -14,33 +16,37 @@ import eu.steffo.twom.theme.iconMaybe
import eu.steffo.twom.theme.iconNoway
import eu.steffo.twom.theme.iconSure
import eu.steffo.twom.theme.iconUnknown
import org.matrix.android.sdk.api.util.JsonDict
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 : JsonDict {
enum class RSVPAnswer {
SURE,
LATER,
MAYBE,
NOWAY,
UNKNOWN,
}
@Composable
fun RSVPAnswer?.toStaticColorRole(): StaticColorRole {
fun RSVPAnswer.toStaticColorRole(): StaticColorRole {
return when (this) {
RSVPAnswer.SURE -> colorRoleSure()
RSVPAnswer.LATER -> colorRoleLater()
RSVPAnswer.MAYBE -> colorRoleMaybe()
RSVPAnswer.NOWAY -> colorRoleNoway()
null -> colorRoleUnknown()
RSVPAnswer.UNKNOWN -> colorRoleUnknown()
}
}
fun RSVPAnswer?.toIcon(): ImageVector {
fun RSVPAnswer.toIcon(): ImageVector {
return when (this) {
RSVPAnswer.SURE -> iconSure
RSVPAnswer.LATER -> iconLater
RSVPAnswer.MAYBE -> iconMaybe
RSVPAnswer.NOWAY -> iconNoway
null -> iconUnknown
RSVPAnswer.UNKNOWN -> iconUnknown
}
}
@ -50,25 +56,72 @@ fun RSVPAnswer.toLabelResourceId(): Int {
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 {
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
null -> R.string.room_rsvp_unknown_response
RSVPAnswer.UNKNOWN -> R.string.room_rsvp_unknown_response
}
}
fun RSVPAnswer?.toPlaceholderResourceId(): Int {
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
null -> R.string.room_rsvp_unknown_placeholder
RSVPAnswer.UNKNOWN -> R.string.room_rsvp_unknown_placeholder
}
}
fun RSVPAnswer.toEmoji(): String {
return when (this) {
RSVPAnswer.SURE -> ""
RSVPAnswer.LATER -> "\uD83D\uDD52"
RSVPAnswer.MAYBE -> ""
RSVPAnswer.NOWAY -> "⛔️"
RSVPAnswer.UNKNOWN -> ""
}
}
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 request = room.stateService().getStateEventLive(
eventType = "eu.steffo.twom.rsvp",
stateKey = QueryStringValue.Equals(userId),
).observeAsState()
return makeRSVP(request)
}

View file

@ -15,14 +15,14 @@ import eu.steffo.twom.theme.TwoMPadding
@Preview
fun RSVPAnswerSelectRow(
modifier: Modifier = Modifier,
value: RSVPAnswer? = null,
onChange: (answer: RSVPAnswer?) -> Unit = {},
value: RSVPAnswer = RSVPAnswer.UNKNOWN,
onChange: (answer: RSVPAnswer) -> Unit = {},
) {
fun toggleSwitch(representing: RSVPAnswer): () -> Unit {
return {
onChange(
when (value) {
representing -> null
representing -> RSVPAnswer.UNKNOWN
else -> representing
}
)
@ -35,7 +35,7 @@ fun RSVPAnswerSelectRow(
) {
Row(
modifier = Modifier
.padding(start = 10.dp, end = 10.dp)
.padding(start = 8.dp, end = 8.dp)
) {
RSVPAnswerFilterChip(
modifier = TwoMPadding.chips,

View file

@ -1,6 +1,5 @@
package eu.steffo.twom.room
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.OutlinedTextFieldDefaults
@ -16,9 +15,9 @@ fun RSVPCommentField(
modifier: Modifier = Modifier,
value: String = "",
onChange: (value: String) -> Unit = {},
currentRsvpAnswer: RSVPAnswer? = null,
rsvpAnswer: RSVPAnswer = RSVPAnswer.UNKNOWN,
) {
val colorRole = currentRsvpAnswer.toStaticColorRole()
val colorRole = rsvpAnswer.toStaticColorRole()
OutlinedTextField(
modifier = modifier,
@ -28,24 +27,17 @@ fun RSVPCommentField(
shape = MaterialTheme.shapes.small,
placeholder = {
Text(
text = stringResource(currentRsvpAnswer.toPlaceholderResourceId())
)
},
colors = if (currentRsvpAnswer != null) {
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,
)
} else {
OutlinedTextFieldDefaults.colors(
focusedBorderColor = LocalContentColor.current,
unfocusedBorderColor = MaterialTheme.colorScheme.surfaceVariant,
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

@ -16,9 +16,9 @@ fun RSVPUpdateButton(
modifier: Modifier = Modifier,
enabled: Boolean = true,
onClick: () -> Unit = {},
currentRsvpAnswer: RSVPAnswer? = null,
rsvpAnswer: RSVPAnswer = RSVPAnswer.UNKNOWN,
) {
val colorRole = currentRsvpAnswer.toStaticColorRole()
val colorRole = rsvpAnswer.toStaticColorRole()
Button(
modifier = modifier,

View file

@ -14,9 +14,10 @@ import androidx.compose.ui.unit.dp
@Composable
@Preview
fun RoomActivityAnswerForm(
currentRsvpAnswer: RSVPAnswer? = null,
currentRsvpAnswer: RSVPAnswer = RSVPAnswer.UNKNOWN,
currentRsvpComment: String = "",
onUpdate: (rsvpAnswer: RSVPAnswer?, rsvpComment: String) -> Unit = { _, _ -> },
onUpdate: (rsvpAnswer: RSVPAnswer, rsvpComment: String) -> Unit = { _, _ -> },
isUpdating: Boolean = false,
) {
var rsvpAnswer by rememberSaveable { mutableStateOf(currentRsvpAnswer) }
var rsvpComment by rememberSaveable { mutableStateOf(currentRsvpComment) }
@ -33,14 +34,14 @@ fun RoomActivityAnswerForm(
.fillMaxWidth(),
value = rsvpComment,
onChange = { rsvpComment = it },
currentRsvpAnswer = rsvpAnswer,
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,
currentRsvpAnswer = rsvpAnswer,
enabled = hasChanged && !isUpdating,
rsvpAnswer = rsvpAnswer,
)
}

View file

@ -1,12 +1,17 @@
package eu.steffo.twom.room
import android.util.Log
import androidx.compose.foundation.layout.Column
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.livedata.observeAsState
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
@ -14,7 +19,7 @@ import eu.steffo.twom.matrix.LocalSession
import eu.steffo.twom.theme.ErrorText
import eu.steffo.twom.theme.TwoMPadding
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.failure.Failure
import kotlin.jvm.optionals.getOrNull
@ -25,52 +30,88 @@ fun RoomActivityContent(
val scope = rememberCoroutineScope()
val session = LocalSession.current
val roomRequest = LocalRoom.current
val room = roomRequest?.getOrNull()
val roomSummaryRequest = LocalRoomSummary.current
val roomSummary = roomSummaryRequest?.getOrNull()
if (session == null) {
ErrorText(stringResource(R.string.room_error_session_missing))
return
}
val myRsvp = room?.stateService()?.getStateEventLive(
eventType = "eu.steffo.twom.rsvp",
stateKey = QueryStringValue.Equals(session!!.myUserId),
)?.observeAsState()
// TODO: I stopped here; how to retrieve the RSVP from this?
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<Failure.ServerError?>(null) }
Column(modifier) {
if (session == null) {
ErrorText(stringResource(R.string.room_error_session_missing))
} else if (roomRequest == null) {
ErrorText(stringResource(R.string.room_error_room_missing))
} else if (!roomRequest.isPresent) {
ErrorText(stringResource(R.string.room_error_room_notfound))
} else if (roomSummaryRequest == null) {
// Loading
} else if (!roomSummaryRequest.hasValue()) {
ErrorText(stringResource(R.string.room_error_room_notfound))
} else if (roomSummary != null) {
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_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,
)
}
Row(TwoMPadding.base) {
Text(
text = stringResource(R.string.room_rsvp_title),
style = MaterialTheme.typography.labelLarge,
)
}
RoomActivityAnswerForm(
currentRsvpAnswer = myRsvp,
currentRsvpComment = myRsvp,
onUpdate = { answer, comment ->
scope.launch SendStateEvent@{
room!!.stateService().sendStateEvent(
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(
@ -80,38 +121,70 @@ fun RoomActivityContent(
)
),
)
} catch (error: Failure.ServerError) {
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: Failure.ServerError) {
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 ?: "",
)
Row(TwoMPadding.base) {
Text(
text = stringResource(R.string.room_invitees_title),
style = MaterialTheme.typography.labelLarge,
)
}
Column(TwoMPadding.base) {
otherRsvpRequests.forEach {
MemberListItem(
memberId = LocalSession.current!!.myUserId,
rsvpAnswer = rsvpAnswer,
rsvpComment = rsvpComment,
)
roomSummary.otherMemberIds.forEach {
MemberListItem(
memberId = it,
rsvpAnswer = null,
rsvpComment = "",
)
}
}
} else if (isError) {
Row(TwoMPadding.base) {
Text(
// TODO: Maybe add a better error string
text = stringResource(R.string.error),
color = MaterialTheme.colorScheme.error,
memberId = it.first,
rsvpAnswer = it.second?.second ?: RSVPAnswer.UNKNOWN,
rsvpComment = it.second?.third ?: "",
)
}
}

View file

@ -8,7 +8,7 @@ fun colorRoleUnknown(): StaticColorRole {
return StaticColorRole(
value = MaterialTheme.colorScheme.inverseSurface,
onValue = MaterialTheme.colorScheme.inverseOnSurface,
valueContainer = MaterialTheme.colorScheme.surfaceVariant,
onValueContainer = MaterialTheme.colorScheme.onSurfaceVariant,
valueContainer = MaterialTheme.colorScheme.surface,
onValueContainer = MaterialTheme.colorScheme.onSurface,
)
}

View file

@ -58,4 +58,7 @@
<string name="room_error_session_missing">The Matrix session context has not been initialized.</string>
<string name="room_error_room_notfound">Could not find the requested Matrix room.</string>
<string name="room_error_room_missing">The Matrix room context has not been initialized.</string>
<string name="room_rsvp_unknown_label">No answer</string>
<string name="room_error_roomsummary_missing">The Matrix room summary context has not been initialized.</string>
<string name="room_error_roomsummary_notfound">Could not find the requested Matrix room summary.</string>
</resources>