From f0a856eb20efa0c1a1dc94b31e98483df7115e0e Mon Sep 17 00:00:00 2001
From: Adam Brown <adampsbrown@gmail.com>
Date: Thu, 17 Mar 2022 23:01:40 +0000
Subject: [PATCH] adding room join/leaving based on sync status - means the
 invitations page should work!

---
 .../st/domain/sync/OverviewPersistence.kt     | 20 ++++++++++++++++
 .../app/dapk/db/model/InviteState.sq          |  6 ++++-
 .../app/dapk/db/model/OverviewState.sq        |  6 ++++-
 .../sqldelight/app/dapk/db/model/RoomEvent.sq |  4 ++++
 .../st/notifications/NotificationsUseCase.kt  |  3 ++-
 .../st/notifications/PushAndroidService.kt    |  2 ++
 .../app/dapk/st/profile/ProfileViewModel.kt   | 24 ++++++++++++++-----
 .../kotlin/app/dapk/st/matrix/sync/Store.kt   |  2 ++
 .../sync/internal/request/ApiSyncResponse.kt  |  1 +
 .../matrix/sync/internal/sync/SyncReducer.kt  | 13 ++++++++--
 .../matrix/sync/internal/sync/SyncUseCase.kt  | 12 +++++++++-
 11 files changed, 81 insertions(+), 12 deletions(-)

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 a6083e5..f20ba4b 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
@@ -29,6 +29,18 @@ internal class OverviewPersistence(
             .map { it.map { json.decodeFromString(RoomOverview.serializer(), it.blob) } }
     }
 
+    override suspend fun removeRooms(roomsToRemove: List<RoomId>) {
+        dispatchers.withIoContext {
+            database.transaction {
+                roomsToRemove.forEach {
+                    database.inviteStateQueries.remove(it.value)
+                    database.overviewStateQueries.remove(it.value)
+                    database.roomEventQueries.remove(it.value)
+                }
+            }
+        }
+    }
+
     override suspend fun persistInvites(invites: List<RoomInvite>) {
         dispatchers.withIoContext {
             database.inviteStateQueries.transaction {
@@ -46,6 +58,14 @@ internal class OverviewPersistence(
             .map { it.map { json.decodeFromString(RoomInvite.serializer(), it.blob) } }
     }
 
+    override suspend fun removeInvites(invites: List<RoomId>) {
+        dispatchers.withIoContext {
+            database.inviteStateQueries.transaction {
+                invites.forEach { database.inviteStateQueries.remove(it.value) }
+            }
+        }
+    }
+
     override suspend fun persist(overviewState: OverviewState) {
         dispatchers.withIoContext {
             database.transaction {
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 67be2b1..d30ddbe 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
@@ -10,4 +10,8 @@ FROM dbInviteState;
 
 insert:
 INSERT OR REPLACE INTO dbInviteState(room_id, blob)
-VALUES (?, ?);
\ No newline at end of file
+VALUES (?, ?);
+
+remove:
+DELETE FROM dbInviteState
+WHERE room_id = ?;
\ No newline at end of file
diff --git a/domains/store/src/main/sqldelight/app/dapk/db/model/OverviewState.sq b/domains/store/src/main/sqldelight/app/dapk/db/model/OverviewState.sq
index afc9da1..100d397 100644
--- a/domains/store/src/main/sqldelight/app/dapk/db/model/OverviewState.sq
+++ b/domains/store/src/main/sqldelight/app/dapk/db/model/OverviewState.sq
@@ -18,4 +18,8 @@ WHERE room_id = ?;
 
 insert:
 INSERT OR REPLACE INTO dbOverviewState(room_id, latest_activity_timestamp_utc, read_marker, blob)
-VALUES (?, ?, ?, ?);
\ No newline at end of file
+VALUES (?, ?, ?, ?);
+
+remove:
+DELETE FROM dbOverviewState
+WHERE room_id = ?;
\ No newline at end of file
diff --git a/domains/store/src/main/sqldelight/app/dapk/db/model/RoomEvent.sq b/domains/store/src/main/sqldelight/app/dapk/db/model/RoomEvent.sq
index 6c10a56..46883cf 100644
--- a/domains/store/src/main/sqldelight/app/dapk/db/model/RoomEvent.sq
+++ b/domains/store/src/main/sqldelight/app/dapk/db/model/RoomEvent.sq
@@ -33,3 +33,7 @@ FROM dbUnreadEvent
 INNER JOIN dbRoomEvent ON dbUnreadEvent.event_id = dbRoomEvent.event_id
 ORDER BY dbRoomEvent.timestamp_utc DESC
 LIMIT 100;
+
+remove:
+DELETE FROM dbRoomEvent
+WHERE room_id = ?;
\ No newline at end of file
diff --git a/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationsUseCase.kt b/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationsUseCase.kt
index ef2d8c2..87cd027 100644
--- a/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationsUseCase.kt
+++ b/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationsUseCase.kt
@@ -32,7 +32,8 @@ class NotificationsUseCase(
                 val asRooms = changes.keys
                 val removedRooms = inferredCurrentNotifications.keys - asRooms
 
-                val onlyContainsRemovals = inferredCurrentNotifications.filterKeys { !removedRooms.contains(it) } == changes.filterKeys { !removedRooms.contains(it) }
+                val onlyContainsRemovals =
+                    inferredCurrentNotifications.filterKeys { !removedRooms.contains(it) } == changes.filterKeys { !removedRooms.contains(it) }
                 inferredCurrentNotifications.clear()
                 inferredCurrentNotifications.putAll(changes)
 
diff --git a/features/notifications/src/main/kotlin/app/dapk/st/notifications/PushAndroidService.kt b/features/notifications/src/main/kotlin/app/dapk/st/notifications/PushAndroidService.kt
index 8b845c7..5c6a0c9 100644
--- a/features/notifications/src/main/kotlin/app/dapk/st/notifications/PushAndroidService.kt
+++ b/features/notifications/src/main/kotlin/app/dapk/st/notifications/PushAndroidService.kt
@@ -28,8 +28,10 @@ class PushAndroidService : FirebaseMessagingService() {
     }
 
     override fun onNewToken(token: String) {
+        log(PUSH, "new push token received")
         GlobalScope.launch {
             module.pushUseCase().registerPush(token)
+            log(PUSH, "token registered")
         }
     }
 
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 3a883db..484cdcf 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
@@ -79,15 +79,15 @@ class ProfileViewModel(
     }
 
     fun acceptRoomInvite(roomId: RoomId) {
-        viewModelScope.launch {
-            roomService.joinRoom(roomId)
-        }
+        launchCatching { roomService.joinRoom(roomId) }.fold(
+            onError = {}
+        )
     }
 
     fun rejectRoomInvite(roomId: RoomId) {
-        viewModelScope.launch {
-            roomService.rejectJoinRoom(roomId)
-        }
+        launchCatching { roomService.rejectJoinRoom(roomId) }.fold(
+            onError = {}
+        )
     }
 
     fun stop() {
@@ -103,3 +103,15 @@ class ProfileViewModel(
     }
 
 }
+
+fun <S, VE, T> DapkViewModel<S, VE>.launchCatching(block: suspend () -> T): LaunchCatching<T> {
+    return object : LaunchCatching<T> {
+        override fun fold(onSuccess: (T) -> Unit, onError: (Throwable) -> Unit) {
+            viewModelScope.launch { runCatching { block() }.fold(onSuccess, onError) }
+        }
+    }
+}
+
+interface LaunchCatching<T> {
+    fun fold(onSuccess: (T) -> Unit = {}, onError: (Throwable) -> Unit = {})
+}
\ No newline at end of file
diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/Store.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/Store.kt
index 97ddf63..f2ba13a 100644
--- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/Store.kt
+++ b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/Store.kt
@@ -28,6 +28,7 @@ interface FilterStore {
 
 interface OverviewStore {
 
+    suspend fun removeRooms(roomsToRemove: List<RoomId>)
     suspend fun persistInvites(invite: List<RoomInvite>)
     suspend fun persist(overviewState: OverviewState)
 
@@ -35,6 +36,7 @@ interface OverviewStore {
 
     fun latest(): Flow<OverviewState>
     fun latestInvites(): Flow<List<RoomInvite>>
+    suspend fun removeInvites(map: List<RoomId>)
 }
 
 interface SyncStore {
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 54414cb..b125924 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
@@ -214,6 +214,7 @@ sealed class ApiToDeviceEvent {
 internal data class ApiSyncRooms(
     @SerialName("join") val join: Map<RoomId, ApiSyncRoom>? = null,
     @SerialName("invite") val invite: Map<RoomId, ApiSyncRoomInvite>? = null,
+    @SerialName("leave") val leave: Map<RoomId, ApiSyncRoom>? = null,
 )
 
 @Serializable
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 f9443b4..0b84495 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
@@ -20,13 +20,16 @@ internal class SyncReducer(
 
     data class ReducerResult(
         val roomState: List<RoomState>,
-        val invites: List<RoomInvite>
+        val invites: List<RoomInvite>,
+        val roomsLeft: List<RoomId>
     )
 
     suspend fun reduce(isInitialSync: Boolean, sideEffects: SideEffectResult, response: ApiSyncResponse, userCredentials: UserCredentials): ReducerResult {
         val directMessages = response.directMessages()
 
         val invites = response.rooms?.invite?.map { roomInvite(it, userCredentials) } ?: emptyList()
+        val roomsLeft = findRoomsLeft(response, userCredentials)
+
         val apiUpdatedRooms = response.rooms?.join?.keepRoomsWithChanges()
         val apiRoomsToProcess = apiUpdatedRooms?.map { (roomId, apiRoom) ->
             logger.matrixLog(SYNC, "reducing: $roomId")
@@ -49,9 +52,15 @@ internal class SyncReducer(
             }
         }
 
-        return ReducerResult((apiRoomsToProcess + roomsWithSideEffects).awaitAll().filterNotNull(), invites)
+        return ReducerResult((apiRoomsToProcess + roomsWithSideEffects).awaitAll().filterNotNull(), invites, roomsLeft)
     }
 
+    private fun findRoomsLeft(response: ApiSyncResponse, userCredentials: UserCredentials) = response.rooms?.leave?.filter {
+        it.value.state.stateEvents.filterIsInstance<ApiTimelineEvent.RoomMember>().any {
+            it.content.membership.isLeave() && it.senderId == userCredentials.userId
+        }
+    }?.map { it.key } ?: emptyList()
+
     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 }
diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/SyncUseCase.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/SyncUseCase.kt
index d56e8a8..ecf0058 100644
--- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/SyncUseCase.kt
+++ b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/SyncUseCase.kt
@@ -46,13 +46,23 @@ internal class SyncUseCase(
                         val nextState = logger.logP("reducing") { syncReducer.reduce(isInitialSync, sideEffects, response, credentials) }
                         val overview = nextState.roomState.map { it.roomOverview }
 
+                        if (nextState.roomsLeft.isNotEmpty()) {
+                            persistence.removeRooms(nextState.roomsLeft)
+                        }
                         if (nextState.invites.isNotEmpty()) {
                             persistence.persistInvites(nextState.invites)
                         }
 
+
                         when {
                             previousState == overview -> previousState.also { logger.matrixLog(SYNC, "no changes, not persisting new state") }
-                            overview.isNotEmpty() -> overview.also { persistence.persist(overview) }
+                            overview.isNotEmpty() -> overview.also {
+                                val newRooms = overview - (previousState ?: emptyList()).toSet()
+                                if (newRooms.isNotEmpty()) {
+                                    persistence.removeInvites(newRooms.map { it.roomId })
+                                }
+                                persistence.persist(overview)
+                            }
                             else -> previousState.also { logger.matrixLog(SYNC, "nothing to do") }
                         }
                     }