accumulating and storing the invite event metadata for member and room names

This commit is contained in:
Adam Brown 2022-03-16 22:36:42 +00:00
parent 423ee3afd9
commit 38c9e33a59
8 changed files with 74 additions and 21 deletions

View File

@ -11,10 +11,8 @@ import app.dapk.st.matrix.sync.RoomInvite
import app.dapk.st.matrix.sync.RoomOverview import app.dapk.st.matrix.sync.RoomOverview
import com.squareup.sqldelight.runtime.coroutines.asFlow import com.squareup.sqldelight.runtime.coroutines.asFlow
import com.squareup.sqldelight.runtime.coroutines.mapToList import com.squareup.sqldelight.runtime.coroutines.mapToList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
private val json = Json private val json = Json
@ -35,7 +33,7 @@ internal class OverviewPersistence(
dispatchers.withIoContext { dispatchers.withIoContext {
database.inviteStateQueries.transaction { database.inviteStateQueries.transaction {
invites.forEach { invites.forEach {
database.inviteStateQueries.insert(it.roomId.value) database.inviteStateQueries.insert(it.roomId.value, json.encodeToString(RoomInvite.serializer(), it))
} }
} }
} }
@ -45,7 +43,7 @@ internal class OverviewPersistence(
return database.inviteStateQueries.selectAll() return database.inviteStateQueries.selectAll()
.asFlow() .asFlow()
.mapToList() .mapToList()
.map { it.map { RoomInvite(RoomId(it)) } } .map { it.map { json.decodeFromString(RoomInvite.serializer(), it.blob) } }
} }
override suspend fun persist(overviewState: OverviewState) { override suspend fun persist(overviewState: OverviewState) {

View File

@ -1,12 +1,13 @@
CREATE TABLE dbInviteState ( CREATE TABLE dbInviteState (
room_id TEXT NOT NULL, room_id TEXT NOT NULL,
blob TEXT NOT NULL,
PRIMARY KEY (room_id) PRIMARY KEY (room_id)
); );
selectAll: selectAll:
SELECT room_id SELECT room_id, blob
FROM dbInviteState; FROM dbInviteState;
insert: insert:
INSERT OR REPLACE INTO dbInviteState(room_id) INSERT OR REPLACE INTO dbInviteState(room_id, blob)
VALUES (?); VALUES (?, ?);

View File

@ -22,6 +22,8 @@ import app.dapk.st.core.LifecycleEffect
import app.dapk.st.core.StartObserving import app.dapk.st.core.StartObserving
import app.dapk.st.core.components.CenteredLoading import app.dapk.st.core.components.CenteredLoading
import app.dapk.st.design.components.* import app.dapk.st.design.components.*
import app.dapk.st.matrix.sync.InviteMeta
import app.dapk.st.matrix.sync.RoomInvite
import app.dapk.st.settings.SettingsActivity import app.dapk.st.settings.SettingsActivity
@Composable @Composable
@ -122,14 +124,20 @@ private fun Invitations(viewModel: ProfileViewModel, invitations: Page.Invitatio
is Lce.Content -> { is Lce.Content -> {
LazyColumn { LazyColumn {
items(state.value) { items(state.value) {
TextRow(title = it.value, includeDivider = false) { val text = when (val meta = it.inviteMeta) {
InviteMeta.DirectMessage -> "${it.inviterName()} has invited you to chat"
is InviteMeta.Room -> "${it.inviterName()} has invited you to ${meta.roomName ?: "unnamed room"}"
}
TextRow(title = text, includeDivider = false) {
Spacer(modifier = Modifier.height(4.dp))
Row { Row {
Button(modifier = Modifier.weight(1f), onClick = { viewModel.acceptRoomInvite(it) }) { Button(modifier = Modifier.weight(1f), onClick = { viewModel.rejectRoomInvite(it.roomId) }) {
Text("Accept".uppercase()) Text("Reject".uppercase())
} }
Spacer(modifier = Modifier.fillMaxWidth(0.1f)) Spacer(modifier = Modifier.fillMaxWidth(0.1f))
Button(modifier = Modifier.weight(1f), onClick = { viewModel.rejectRoomInvite(it) }) { Button(modifier = Modifier.weight(1f), onClick = { viewModel.acceptRoomInvite(it.roomId) }) {
Text("Reject".uppercase()) Text("Accept".uppercase())
} }
} }
} }
@ -141,6 +149,8 @@ private fun Invitations(viewModel: ProfileViewModel, invitations: Page.Invitatio
} }
} }
private fun RoomInvite.inviterName() = this.from.displayName?.let { "$it (${this.from.id.value})" } ?: this.from.id.value
@Composable @Composable
private fun ProfileViewModel.ObserveEvents() { private fun ProfileViewModel.ObserveEvents() {
val context = LocalContext.current val context = LocalContext.current

View File

@ -5,6 +5,7 @@ import app.dapk.st.design.components.Route
import app.dapk.st.design.components.SpiderPage import app.dapk.st.design.components.SpiderPage
import app.dapk.st.matrix.common.RoomId import app.dapk.st.matrix.common.RoomId
import app.dapk.st.matrix.room.ProfileService import app.dapk.st.matrix.room.ProfileService
import app.dapk.st.matrix.sync.RoomInvite
data class ProfileScreenState( data class ProfileScreenState(
val page: SpiderPage<out Page>, val page: SpiderPage<out Page>,
@ -18,7 +19,7 @@ sealed interface Page {
) )
} }
data class Invitations(val content: Lce<List<RoomId>>): Page data class Invitations(val content: Lce<List<RoomInvite>>): Page
object Routes { object Routes {
val profile = Route<Profile>("Profile") val profile = Route<Profile>("Profile")

View File

@ -50,7 +50,7 @@ class ProfileViewModel(
syncService.invites() syncService.invites()
.onEach { .onEach {
updatePageState<Page.Invitations> { updatePageState<Page.Invitations> {
copy(content = Lce.Content(it.map { it.roomId })) copy(content = Lce.Content(it))
} }
} }
.launchPageJob() .launchPageJob()

View File

@ -31,5 +31,18 @@ data class LastMessage(
@Serializable @Serializable
data class RoomInvite( data class RoomInvite(
@SerialName("from") val from: RoomMember,
@SerialName("room_id") val roomId: RoomId, @SerialName("room_id") val roomId: RoomId,
@SerialName("meta") val inviteMeta: InviteMeta,
) )
@Serializable
sealed class InviteMeta {
@Serializable
@SerialName("direct_message")
object DirectMessage : InviteMeta()
@Serializable
@SerialName("room")
data class Room(val roomName: String? = null) : InviteMeta()
}

View File

@ -230,14 +230,30 @@ internal data class ApiInviteEvents(
sealed class ApiStrippedEvent { sealed class ApiStrippedEvent {
@Serializable @Serializable
@SerialName("m.room.create") @SerialName("m.room.member")
internal data class RoomCreate( internal data class RoomMember(
@SerialName("content") val content: Content,
@SerialName("sender") val sender: UserId,
) : ApiStrippedEvent() {
@Serializable
internal data class Content(
@SerialName("displayname") val displayName: String? = null,
@SerialName("membership") val membership: ApiTimelineEvent.RoomMember.Content.Membership? = null,
@SerialName("is_direct") val isDirect: Boolean? = null,
@SerialName("avatar_url") val avatarUrl: MxUrl? = null,
)
}
@Serializable
@SerialName("m.room.name")
internal data class RoomName(
@SerialName("content") val content: Content, @SerialName("content") val content: Content,
) : ApiStrippedEvent() { ) : ApiStrippedEvent() {
@Serializable @Serializable
internal data class Content( internal data class Content(
@SerialName("type") val type: String? = null @SerialName("name") val name: String? = null
) )
} }
@ -407,6 +423,7 @@ internal sealed class ApiTimelineEvent {
value class Membership(val value: String) { value class Membership(val value: String) {
fun isJoin() = value == "join" fun isJoin() = value == "join"
fun isInvite() = value == "invite" fun isInvite() = value == "invite"
fun isLeave() = value == "leave"
} }
} }

View File

@ -4,11 +4,10 @@ import app.dapk.st.core.CoroutineDispatchers
import app.dapk.st.core.withIoContextAsync import app.dapk.st.core.withIoContextAsync
import app.dapk.st.matrix.common.* import app.dapk.st.matrix.common.*
import app.dapk.st.matrix.common.MatrixLogTag.SYNC import app.dapk.st.matrix.common.MatrixLogTag.SYNC
import app.dapk.st.matrix.sync.InviteMeta
import app.dapk.st.matrix.sync.RoomInvite import app.dapk.st.matrix.sync.RoomInvite
import app.dapk.st.matrix.sync.RoomState import app.dapk.st.matrix.sync.RoomState
import app.dapk.st.matrix.sync.internal.request.ApiAccountEvent import app.dapk.st.matrix.sync.internal.request.*
import app.dapk.st.matrix.sync.internal.request.ApiSyncResponse
import app.dapk.st.matrix.sync.internal.request.ApiSyncRoom
import app.dapk.st.matrix.sync.internal.room.SideEffectResult import app.dapk.st.matrix.sync.internal.room.SideEffectResult
import kotlinx.coroutines.awaitAll import kotlinx.coroutines.awaitAll
@ -27,7 +26,7 @@ internal class SyncReducer(
suspend fun reduce(isInitialSync: Boolean, sideEffects: SideEffectResult, response: ApiSyncResponse, userCredentials: UserCredentials): ReducerResult { suspend fun reduce(isInitialSync: Boolean, sideEffects: SideEffectResult, response: ApiSyncResponse, userCredentials: UserCredentials): ReducerResult {
val directMessages = response.directMessages() val directMessages = response.directMessages()
val invites = response.rooms?.invite?.keys?.map { RoomInvite(it) } ?: emptyList() val invites = response.rooms?.invite?.map { roomInvite(it, userCredentials) } ?: emptyList()
val apiUpdatedRooms = response.rooms?.join?.keepRoomsWithChanges() val apiUpdatedRooms = response.rooms?.join?.keepRoomsWithChanges()
val apiRoomsToProcess = apiUpdatedRooms?.map { (roomId, apiRoom) -> val apiRoomsToProcess = apiUpdatedRooms?.map { (roomId, apiRoom) ->
logger.matrixLog(SYNC, "reducing: $roomId") logger.matrixLog(SYNC, "reducing: $roomId")
@ -52,6 +51,20 @@ internal class SyncReducer(
return ReducerResult((apiRoomsToProcess + roomsWithSideEffects).awaitAll().filterNotNull(), invites) return ReducerResult((apiRoomsToProcess + roomsWithSideEffects).awaitAll().filterNotNull(), invites)
} }
private fun roomInvite(entry: Map.Entry<RoomId, ApiSyncRoomInvite>, userCredentials: UserCredentials): RoomInvite {
val memberEvents = entry.value.state.events.filterIsInstance<ApiStrippedEvent.RoomMember>()
val invitee = memberEvents.first { it.content.membership?.isInvite() ?: false }
val from = memberEvents.first { it.sender == invitee.sender }
return RoomInvite(
RoomMember(from.sender, from.content.displayName, from.content.avatarUrl?.convertMxUrToUrl(userCredentials.homeServer)?.let { AvatarUrl(it) }),
roomId = entry.key,
inviteMeta = when (invitee.content.isDirect) {
true -> InviteMeta.DirectMessage
null, false -> InviteMeta.Room(entry.value.state.events.filterIsInstance<ApiStrippedEvent.RoomName>().firstOrNull()?.content?.name)
},
)
}
} }
private fun Map<RoomId, ApiSyncRoom>.keepRoomsWithChanges() = this.filter { private fun Map<RoomId, ApiSyncRoom>.keepRoomsWithChanges() = this.filter {