diff --git a/domains/store/src/main/kotlin/app/dapk/st/domain/sync/OverviewPersistence.kt b/domains/store/src/main/kotlin/app/dapk/st/domain/sync/OverviewPersistence.kt index 1b1ee08..a6083e5 100644 --- a/domains/store/src/main/kotlin/app/dapk/st/domain/sync/OverviewPersistence.kt +++ b/domains/store/src/main/kotlin/app/dapk/st/domain/sync/OverviewPersistence.kt @@ -11,10 +11,8 @@ import app.dapk.st.matrix.sync.RoomInvite import app.dapk.st.matrix.sync.RoomOverview import com.squareup.sqldelight.runtime.coroutines.asFlow import com.squareup.sqldelight.runtime.coroutines.mapToList -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map -import kotlinx.coroutines.withContext import kotlinx.serialization.json.Json private val json = Json @@ -35,7 +33,7 @@ internal class OverviewPersistence( dispatchers.withIoContext { database.inviteStateQueries.transaction { 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() .asFlow() .mapToList() - .map { it.map { RoomInvite(RoomId(it)) } } + .map { it.map { json.decodeFromString(RoomInvite.serializer(), it.blob) } } } override suspend fun persist(overviewState: OverviewState) { diff --git a/domains/store/src/main/sqldelight/app/dapk/db/model/InviteState.sq b/domains/store/src/main/sqldelight/app/dapk/db/model/InviteState.sq index d29ad00..67be2b1 100644 --- a/domains/store/src/main/sqldelight/app/dapk/db/model/InviteState.sq +++ b/domains/store/src/main/sqldelight/app/dapk/db/model/InviteState.sq @@ -1,12 +1,13 @@ CREATE TABLE dbInviteState ( room_id TEXT NOT NULL, + blob TEXT NOT NULL, PRIMARY KEY (room_id) ); selectAll: -SELECT room_id +SELECT room_id, blob FROM dbInviteState; insert: -INSERT OR REPLACE INTO dbInviteState(room_id) -VALUES (?); \ No newline at end of file +INSERT OR REPLACE INTO dbInviteState(room_id, blob) +VALUES (?, ?); \ No newline at end of file diff --git a/features/profile/src/main/kotlin/app/dapk/st/profile/ProfileScreen.kt b/features/profile/src/main/kotlin/app/dapk/st/profile/ProfileScreen.kt index 95fca32..e3a0f0d 100644 --- a/features/profile/src/main/kotlin/app/dapk/st/profile/ProfileScreen.kt +++ b/features/profile/src/main/kotlin/app/dapk/st/profile/ProfileScreen.kt @@ -22,6 +22,8 @@ import app.dapk.st.core.LifecycleEffect import app.dapk.st.core.StartObserving import app.dapk.st.core.components.CenteredLoading 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 @Composable @@ -122,14 +124,20 @@ private fun Invitations(viewModel: ProfileViewModel, invitations: Page.Invitatio is Lce.Content -> { LazyColumn { 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 { - Button(modifier = Modifier.weight(1f), onClick = { viewModel.acceptRoomInvite(it) }) { - Text("Accept".uppercase()) + Button(modifier = Modifier.weight(1f), onClick = { viewModel.rejectRoomInvite(it.roomId) }) { + Text("Reject".uppercase()) } Spacer(modifier = Modifier.fillMaxWidth(0.1f)) - Button(modifier = Modifier.weight(1f), onClick = { viewModel.rejectRoomInvite(it) }) { - Text("Reject".uppercase()) + Button(modifier = Modifier.weight(1f), onClick = { viewModel.acceptRoomInvite(it.roomId) }) { + 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 private fun ProfileViewModel.ObserveEvents() { val context = LocalContext.current diff --git a/features/profile/src/main/kotlin/app/dapk/st/profile/ProfileState.kt b/features/profile/src/main/kotlin/app/dapk/st/profile/ProfileState.kt index 617e6c8..5e0b6e2 100644 --- a/features/profile/src/main/kotlin/app/dapk/st/profile/ProfileState.kt +++ b/features/profile/src/main/kotlin/app/dapk/st/profile/ProfileState.kt @@ -5,6 +5,7 @@ import app.dapk.st.design.components.Route import app.dapk.st.design.components.SpiderPage import app.dapk.st.matrix.common.RoomId import app.dapk.st.matrix.room.ProfileService +import app.dapk.st.matrix.sync.RoomInvite data class ProfileScreenState( val page: SpiderPage, @@ -18,7 +19,7 @@ sealed interface Page { ) } - data class Invitations(val content: Lce>): Page + data class Invitations(val content: Lce>): Page object Routes { val profile = Route("Profile") diff --git a/features/profile/src/main/kotlin/app/dapk/st/profile/ProfileViewModel.kt b/features/profile/src/main/kotlin/app/dapk/st/profile/ProfileViewModel.kt index eca7906..3a883db 100644 --- a/features/profile/src/main/kotlin/app/dapk/st/profile/ProfileViewModel.kt +++ b/features/profile/src/main/kotlin/app/dapk/st/profile/ProfileViewModel.kt @@ -50,7 +50,7 @@ class ProfileViewModel( syncService.invites() .onEach { updatePageState { - copy(content = Lce.Content(it.map { it.roomId })) + copy(content = Lce.Content(it)) } } .launchPageJob() diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/OverviewState.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/OverviewState.kt index b914799..75bf8ad 100644 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/OverviewState.kt +++ b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/OverviewState.kt @@ -31,5 +31,18 @@ data class LastMessage( @Serializable data class RoomInvite( + @SerialName("from") val from: RoomMember, @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() +} diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/request/ApiSyncResponse.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/request/ApiSyncResponse.kt index f068675..54414cb 100644 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/request/ApiSyncResponse.kt +++ b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/request/ApiSyncResponse.kt @@ -230,14 +230,30 @@ internal data class ApiInviteEvents( sealed class ApiStrippedEvent { @Serializable - @SerialName("m.room.create") - internal data class RoomCreate( + @SerialName("m.room.member") + 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, ) : ApiStrippedEvent() { @Serializable 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) { fun isJoin() = value == "join" fun isInvite() = value == "invite" + fun isLeave() = value == "leave" } } diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/SyncReducer.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/SyncReducer.kt index 24eebc9..f9443b4 100644 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/SyncReducer.kt +++ b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/SyncReducer.kt @@ -4,11 +4,10 @@ import app.dapk.st.core.CoroutineDispatchers import app.dapk.st.core.withIoContextAsync import app.dapk.st.matrix.common.* 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.RoomState -import app.dapk.st.matrix.sync.internal.request.ApiAccountEvent -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.request.* import app.dapk.st.matrix.sync.internal.room.SideEffectResult import kotlinx.coroutines.awaitAll @@ -27,7 +26,7 @@ internal class SyncReducer( suspend fun reduce(isInitialSync: Boolean, sideEffects: SideEffectResult, response: ApiSyncResponse, userCredentials: UserCredentials): ReducerResult { 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 apiRoomsToProcess = apiUpdatedRooms?.map { (roomId, apiRoom) -> logger.matrixLog(SYNC, "reducing: $roomId") @@ -52,6 +51,20 @@ internal class SyncReducer( return ReducerResult((apiRoomsToProcess + roomsWithSideEffects).awaitAll().filterNotNull(), invites) } + + private fun roomInvite(entry: Map.Entry, userCredentials: UserCredentials): RoomInvite { + val memberEvents = entry.value.state.events.filterIsInstance() + 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().firstOrNull()?.content?.name) + }, + ) + } } private fun Map.keepRoomsWithChanges() = this.filter {