From d54cc2881d988b53e362db23fbfe5185a2e0b194 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 13 Jan 2025 10:52:57 +0000 Subject: [PATCH 1/5] =?UTF-8?q?fix:=20messages=20migration=20from=20proteu?= =?UTF-8?q?s=20to=20mls=20[WPB-15149]=20=F0=9F=8D=92=20(#3219)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Commit with unresolved merge conflicts * test fixes --------- Co-authored-by: Jakub Żerko Co-authored-by: Yamil Medina --- .../conversation/mls/OneOnOneMigrator.kt | 9 + .../com/wire/kalium/persistence/Messages.sq | 2 +- .../com/wire/kalium/persistence/Reactions.sq | 2 +- .../src/commonMain/db_user/migrations/94.sqm | 47 ++++ .../persistence/dao/message/MessageDAOTest.kt | 31 +++ .../persistence/utils/stubs/MessageStubs.kt | 243 ++++++++++++++++++ 6 files changed, 332 insertions(+), 2 deletions(-) create mode 100644 persistence/src/commonMain/db_user/migrations/94.sqm diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/mls/OneOnOneMigrator.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/mls/OneOnOneMigrator.kt index 2022a62263f..4321959532c 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/mls/OneOnOneMigrator.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/mls/OneOnOneMigrator.kt @@ -89,6 +89,11 @@ internal class OneOnOneMigratorImpl( return getResolvedMLSOneOnOne(user.id) .flatMap { mlsConversation -> if (user.activeOneOnOneConversationId == mlsConversation) { + kaliumLogger.d( + "active one-on-one already resolved to MLS " + + "${mlsConversation.toLogString()}, " + + "user = ${user.id.toLogString()}" + ) return@flatMap Either.Right(mlsConversation) } @@ -136,6 +141,10 @@ internal class OneOnOneMigratorImpl( // We can theoretically have more than one proteus 1-1 conversation with // team members since there was no backend safeguards against this proteusOneOnOneConversations.foldToEitherWhileRight(Unit) { proteusOneOnOneConversation, _ -> + kaliumLogger.d( + "migrating proteus ${proteusOneOnOneConversation.toLogString()} " + + "to MLS conv ${targetConversation.toLogString()}" + ) messageRepository.moveMessagesToAnotherConversation( originalConversation = proteusOneOnOneConversation, targetConversation = targetConversation diff --git a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Messages.sq b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Messages.sq index a25d9a938bb..96c7c7addd7 100644 --- a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Messages.sq +++ b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Messages.sq @@ -91,7 +91,7 @@ CREATE TABLE MessageRestrictedAssetContent ( asset_size INTEGER NOT NULL, asset_name TEXT NOT NULL, - FOREIGN KEY (message_id, conversation_id) REFERENCES Message(id, conversation_id) ON DELETE CASCADE, + FOREIGN KEY (message_id, conversation_id) REFERENCES Message(id, conversation_id) ON DELETE CASCADE ON UPDATE CASCADE, PRIMARY KEY (message_id, conversation_id) ); diff --git a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Reactions.sq b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Reactions.sq index 50e7c3a23bb..983d9c1ce71 100644 --- a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Reactions.sq +++ b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Reactions.sq @@ -6,7 +6,7 @@ CREATE TABLE Reaction ( sender_id TEXT AS QualifiedIDEntity NOT NULL, emoji TEXT NOT NULL, date TEXT NOT NULL, - FOREIGN KEY (message_id, conversation_id) REFERENCES Message(id, conversation_id) ON DELETE CASCADE, + FOREIGN KEY (message_id, conversation_id) REFERENCES Message(id, conversation_id) ON DELETE CASCADE ON UPDATE CASCADE, FOREIGN KEY (sender_id) REFERENCES User(qualified_id) ON DELETE CASCADE, PRIMARY KEY (message_id, conversation_id, sender_id, emoji) ); diff --git a/persistence/src/commonMain/db_user/migrations/94.sqm b/persistence/src/commonMain/db_user/migrations/94.sqm new file mode 100644 index 00000000000..386072d971b --- /dev/null +++ b/persistence/src/commonMain/db_user/migrations/94.sqm @@ -0,0 +1,47 @@ +CREATE TABLE Reaction_temp AS +SELECT * FROM Reaction; + +DROP TABLE Reaction; + +CREATE TABLE Reaction ( + message_id TEXT NOT NULL, + conversation_id TEXT AS QualifiedIDEntity NOT NULL, + sender_id TEXT AS QualifiedIDEntity NOT NULL, + emoji TEXT NOT NULL, + date TEXT NOT NULL, + FOREIGN KEY (message_id, conversation_id) REFERENCES Message(id, conversation_id) ON DELETE CASCADE ON UPDATE CASCADE, + FOREIGN KEY (sender_id) REFERENCES User(qualified_id) ON DELETE CASCADE, + PRIMARY KEY (message_id, conversation_id, sender_id, emoji) +); + +INSERT INTO Reaction(message_id, conversation_id, sender_id, emoji, date) +SELECT message_id, conversation_id, sender_id, emoji, date +FROM Reaction_temp; + +DROP TABLE Reaction_temp; + +CREATE INDEX reaction_sender_index ON Reaction(sender_id); +CREATE INDEX reaction_emoji_index ON Reaction(emoji); + +CREATE TABLE MessageRestrictedAssetContent_temp AS +SELECT * FROM MessageRestrictedAssetContent; + +DROP TABLE MessageRestrictedAssetContent; + +CREATE TABLE MessageRestrictedAssetContent ( + message_id TEXT NOT NULL, + conversation_id TEXT AS QualifiedIDEntity NOT NULL, + + asset_mime_type TEXT NOT NULL, + asset_size INTEGER NOT NULL, + asset_name TEXT NOT NULL, + + FOREIGN KEY (message_id, conversation_id) REFERENCES Message(id, conversation_id) ON DELETE CASCADE ON UPDATE CASCADE, + PRIMARY KEY (message_id, conversation_id) +); + +INSERT INTO MessageRestrictedAssetContent(message_id, conversation_id, asset_mime_type, asset_size, asset_name) +SELECT message_id, conversation_id, asset_mime_type, asset_size, asset_name +FROM MessageRestrictedAssetContent_temp; + +DROP TABLE MessageRestrictedAssetContent_temp; diff --git a/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/message/MessageDAOTest.kt b/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/message/MessageDAOTest.kt index a365283d308..a0ea8d22200 100644 --- a/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/message/MessageDAOTest.kt +++ b/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/message/MessageDAOTest.kt @@ -27,10 +27,12 @@ import com.wire.kalium.persistence.dao.asset.AssetEntity import com.wire.kalium.persistence.dao.asset.AssetTransferStatusEntity import com.wire.kalium.persistence.dao.conversation.ConversationDAO import com.wire.kalium.persistence.dao.conversation.ConversationEntity +import com.wire.kalium.persistence.dao.reaction.ReactionDAO import com.wire.kalium.persistence.dao.receipt.ReceiptDAO import com.wire.kalium.persistence.dao.receipt.ReceiptTypeEntity import com.wire.kalium.persistence.dao.unread.UnreadEventTypeEntity import com.wire.kalium.persistence.utils.IgnoreIOS +import com.wire.kalium.persistence.utils.stubs.allMessageEntities import com.wire.kalium.persistence.utils.stubs.newConversationEntity import com.wire.kalium.persistence.utils.stubs.newRegularMessageEntity import com.wire.kalium.persistence.utils.stubs.newSystemMessageEntity @@ -49,6 +51,7 @@ import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertIs import kotlin.test.assertNotNull +import kotlin.test.assertNull import kotlin.test.assertTrue import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.seconds @@ -61,6 +64,7 @@ class MessageDAOTest : BaseDatabaseTest() { private lateinit var userDAO: UserDAO private lateinit var receiptDao: ReceiptDAO private lateinit var assetDao: AssetDAO + private lateinit var reactionDao: ReactionDAO private val conversationEntity1 = newConversationEntity("Test1") private val conversationEntity2 = newConversationEntity("Test2") @@ -80,6 +84,7 @@ class MessageDAOTest : BaseDatabaseTest() { userDAO = db.userDAO receiptDao = db.receiptDAO assetDao = db.assetDAO + reactionDao = db.reactionDAO } @Test @@ -2454,6 +2459,32 @@ class MessageDAOTest : BaseDatabaseTest() { assertEquals("3", result) } + @Test + fun givenAllTypesOfMessages_whenMovingToAnotherConversation_thenItSucceeds() = runTest { + // Given + insertInitialData() + val messages = allMessageEntities(conversationId = conversationEntity1.id, senderUserId = userEntity1.id) + val firstEmoji = "🫡" + messageDAO.insertOrIgnoreMessages(messages) + reactionDao.insertReaction( + messages.first().id, + messages.first().conversationId, + userEntity1.id, + Instant.DISTANT_PAST, + firstEmoji + ) + + // When + val exception = kotlin.runCatching { + messageDAO.moveMessages(conversationEntity1.id, conversationEntity2.id) + }.exceptionOrNull() + + // Then + assertNull(exception, "Expected no exception but got: ${exception?.message}") + val result = messageDAO.getMessagesByConversationAndVisibility(conversationEntity2.id, 100, 0).first() + assertEquals(messages.size, result.size) + } + private suspend fun insertInitialData() { userDAO.upsertUsers(listOf(userEntity1, userEntity2)) conversationDAO.insertConversation( diff --git a/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/utils/stubs/MessageStubs.kt b/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/utils/stubs/MessageStubs.kt index 5c2760274a7..31f973cf5f0 100644 --- a/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/utils/stubs/MessageStubs.kt +++ b/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/utils/stubs/MessageStubs.kt @@ -21,10 +21,13 @@ package com.wire.kalium.persistence.utils.stubs import com.wire.kalium.persistence.dao.QualifiedIDEntity import com.wire.kalium.persistence.dao.UserDetailsEntity import com.wire.kalium.persistence.dao.UserIDEntity +import com.wire.kalium.persistence.dao.conversation.ConversationEntity +import com.wire.kalium.persistence.dao.message.ButtonEntity import com.wire.kalium.persistence.dao.message.MessageEntity import com.wire.kalium.persistence.dao.message.MessageEntityContent import com.wire.kalium.persistence.dao.message.draft.MessageDraftEntity import kotlinx.datetime.Instant +import kotlin.random.Random @Suppress("LongParameterList") fun newRegularMessageEntity( @@ -93,3 +96,243 @@ fun newDraftMessageEntity( quotedMessageId: String? = null, selectedMentionList: List = emptyList() ) = MessageDraftEntity(conversationId, text, editMessageId, quotedMessageId, selectedMentionList) + +fun allMessageEntities( + conversationId: QualifiedIDEntity = QualifiedIDEntity("convId", "convDomain"), + senderUserId: QualifiedIDEntity, +): List { + return listOf( + newRegularMessageEntity( + conversationId = conversationId, + senderUserId = senderUserId, + id = "testMessage1", + content = MessageEntityContent.Text( + "@John @John", + linkPreview = listOf( + MessageEntity.LinkPreview( + "https://www.wire.com", + 0, + "https://www.wire.com", + "Wire", + "Wire is the most secure collaboration platform", + ) + ), + mentions = listOf( + MessageEntity.Mention(0, 4, QualifiedIDEntity("senderId", "senderDomain")), + MessageEntity.Mention(6, 10, QualifiedIDEntity("senderId", "senderDomain")) + ), + quotedMessageId = "testMessage2", + isQuoteVerified = true, + quotedMessage = null + ) + ), + + newRegularMessageEntity( + conversationId = conversationId, + senderUserId = senderUserId, + id = "testMessage2", + content = MessageEntityContent.Asset( + 1000, + assetName = "test name", + assetMimeType = "image/png", + assetOtrKey = byteArrayOf(1), + assetSha256Key = byteArrayOf(1), + assetId = "assetId", + assetToken = "", + assetDomain = "convDomain", + assetEncryptionAlgorithm = "", + assetWidth = null, + assetHeight = 0, + ), + ), + newRegularMessageEntity( + conversationId = conversationId, + senderUserId = senderUserId, + id = "testMessage3", + content = MessageEntityContent.Knock(false) + ), + newRegularMessageEntity( + conversationId = conversationId, + senderUserId = senderUserId, + id = "testMessage4", + content = MessageEntityContent.Location( + latitude = 42.0f, + longitude = -42.0f, + name = "someSecretLocation", + zoom = 20 + ) + ), + newRegularMessageEntity( + conversationId = conversationId, + senderUserId = senderUserId, + id = "testMessage5", + content = MessageEntityContent.Unknown(typeName = null, Random.nextBytes(1000)) + ), + newRegularMessageEntity( + conversationId = conversationId, + senderUserId = senderUserId, + id = "testMessage6", + content = MessageEntityContent.FailedDecryption( + null, + 333, + false, + QualifiedIDEntity("senderId", "senderDomain"), + "someClient" + ) + ), + newSystemMessageEntity( + conversationId = conversationId, + senderUserId = senderUserId, + id = "testMessage7", + content = MessageEntityContent.MLSWrongEpochWarning + ), + newSystemMessageEntity( + conversationId = conversationId, + senderUserId = senderUserId, + id = "testMessage8", + content = MessageEntityContent.MemberChange( + listOf(UserIDEntity("value", "domain")), + MessageEntity.MemberChangeType.REMOVED + ) + ), + newRegularMessageEntity( + conversationId = conversationId, + senderUserId = senderUserId, + id = "testMessage9", + content = MessageEntityContent.RestrictedAsset("", 0, "name") + ), + newRegularMessageEntity( + conversationId = conversationId, + senderUserId = senderUserId, + id = "testMessage10", + content = MessageEntityContent.Composite( + MessageEntityContent.Text("text"), + listOf( + ButtonEntity("text1", "id1", false), + ButtonEntity("tex2", "id2", false), + ButtonEntity("tex3", "id3", false), + ButtonEntity("tex4", "id4", false) + ) + ) + ), + newSystemMessageEntity( + conversationId = conversationId, + senderUserId = senderUserId, + id = "testMessage11", + content = MessageEntityContent.MissedCall + ), + newSystemMessageEntity( + conversationId = conversationId, + senderUserId = senderUserId, + id = "testMessage12", + content = MessageEntityContent.CryptoSessionReset + ), + newSystemMessageEntity( + conversationId = conversationId, + senderUserId = senderUserId, + id = "testMessage13", + content = MessageEntityContent.ConversationRenamed("newName") + ), + newSystemMessageEntity( + conversationId = conversationId, + senderUserId = senderUserId, + id = "testMessage14", + content = MessageEntityContent.TeamMemberRemoved("someUser") + ), + newSystemMessageEntity( + conversationId = conversationId, + senderUserId = senderUserId, + id = "testMessage15", + content = MessageEntityContent.NewConversationReceiptMode(true) + ), + newSystemMessageEntity( + conversationId = conversationId, + senderUserId = senderUserId, + id = "testMessage16", + content = MessageEntityContent.ConversationReceiptModeChanged(false) + ), + newSystemMessageEntity( + conversationId = conversationId, + senderUserId = senderUserId, + id = "testMessage17", + content = MessageEntityContent.ConversationMessageTimerChanged(6000) + ), + newSystemMessageEntity( + conversationId = conversationId, + senderUserId = senderUserId, + id = "testMessage18", + content = MessageEntityContent.ConversationProtocolChanged(ConversationEntity.Protocol.MIXED) + ), + newSystemMessageEntity( + conversationId = conversationId, + senderUserId = senderUserId, + id = "testMessage19", + content = MessageEntityContent.ConversationProtocolChangedDuringACall + ), + newSystemMessageEntity( + conversationId = conversationId, + senderUserId = senderUserId, + id = "testMessage20", + content = MessageEntityContent.HistoryLostProtocolChanged + ), + newSystemMessageEntity( + conversationId = conversationId, + senderUserId = senderUserId, + id = "testMessage21", + content = MessageEntityContent.HistoryLost + ), + newSystemMessageEntity( + conversationId = conversationId, + senderUserId = senderUserId, + id = "testMessage22", + content = MessageEntityContent.ConversationCreated + ), + newSystemMessageEntity( + conversationId = conversationId, + senderUserId = senderUserId, + id = "testMessage23", + content = MessageEntityContent.ConversationDegradedMLS + ), + newSystemMessageEntity( + conversationId = conversationId, + senderUserId = senderUserId, + id = "testMessage24", + content = MessageEntityContent.ConversationVerifiedMLS + ), + newSystemMessageEntity( + conversationId = conversationId, + senderUserId = senderUserId, + id = "testMessage25", + content = MessageEntityContent.ConversationDegradedProteus + ), + newSystemMessageEntity( + conversationId = conversationId, + senderUserId = senderUserId, + id = "testMessage26", + content = MessageEntityContent.ConversationVerifiedProteus + ), + newSystemMessageEntity( + conversationId = conversationId, + senderUserId = senderUserId, + id = "testMessage27", + content = MessageEntityContent.ConversationStartedUnverifiedWarning + ), + newSystemMessageEntity( + conversationId = conversationId, + senderUserId = senderUserId, + id = "testMessage28", + content = MessageEntityContent.Federation( + listOf("otherDomain"), + MessageEntity.FederationType.DELETE + ) + ), + newSystemMessageEntity( + conversationId = conversationId, + senderUserId = senderUserId, + id = "testMessage29", + content = MessageEntityContent.LegalHold( + listOf(QualifiedIDEntity("otherId", "otherDomain")), MessageEntity.LegalHoldType.ENABLED_FOR_MEMBERS + ) + ), + ) +} From 47d60696ea7179a7427bf7d00477e518df30a8c0 Mon Sep 17 00:00:00 2001 From: Mohamad Jaara Date: Mon, 13 Jan 2025 12:33:40 +0100 Subject: [PATCH 2/5] refactor: remove the fetch of self user when only the ID is used [WPB-3726] (#3196) * refactor: remove the fetch of self user when only the ID is used * Update UserRepository.kt * remove unwanted logs * detekt * correct the TODO * fix test * detekt * fix test class --- .../kalium/logic/feature/call/CallManagerTest.kt | 10 ++++------ .../logic/feature/call/GlobalCallManager.kt | 2 -- .../kalium/logic/feature/call/CallManagerImpl.kt | 16 +++++----------- .../logic/feature/call/GlobalCallManager.kt | 4 +--- .../logic/data/call/CallingParticipantsOrder.kt | 5 ++--- .../kalium/logic/data/user/UserRepository.kt | 8 +++++--- .../kalium/logic/feature/UserSessionScope.kt | 4 ++-- .../wire/kalium/logic/feature/call/CallsScope.kt | 8 +++++--- .../logic/feature/call/GlobalCallManager.kt | 2 -- .../call/usecase/GetIncomingCallsUseCase.kt | 2 +- .../feature/conversation/ConversationScope.kt | 2 +- .../conversation/MembersToMentionUseCase.kt | 6 +++--- .../kalium/logic/feature/debug/DebugScope.kt | 8 ++++---- .../feature/debug/SendConfirmationUseCase.kt | 10 ++++------ .../data/call/CallingParticipantsOrderTest.kt | 15 ++++++--------- .../conversation/MembersToMentionUseCaseTest.kt | 8 ++------ 16 files changed, 45 insertions(+), 65 deletions(-) diff --git a/logic/src/androidInstrumentedTest/kotlin/com/wire/kalium/logic/feature/call/CallManagerTest.kt b/logic/src/androidInstrumentedTest/kotlin/com/wire/kalium/logic/feature/call/CallManagerTest.kt index 7435be07478..1d6470e00ba 100644 --- a/logic/src/androidInstrumentedTest/kotlin/com/wire/kalium/logic/feature/call/CallManagerTest.kt +++ b/logic/src/androidInstrumentedTest/kotlin/com/wire/kalium/logic/feature/call/CallManagerTest.kt @@ -34,7 +34,6 @@ import com.wire.kalium.logic.data.id.QualifiedIdMapper import com.wire.kalium.logic.data.message.Message import com.wire.kalium.logic.data.message.MessageContent import com.wire.kalium.logic.data.user.UserId -import com.wire.kalium.logic.data.user.UserRepository import com.wire.kalium.logic.feature.call.usecase.ConversationClientsInCallUpdater import com.wire.kalium.logic.feature.call.usecase.CreateAndPersistRecentlyEndedCallMetadataUseCase import com.wire.kalium.logic.feature.call.usecase.GetCallConversationTypeProvider @@ -59,11 +58,10 @@ class CallManagerTest { @Mock private val calling = mock(Calling::class) - @Mock - private val callRepository = mock(CallRepository::class) + private val selfUserId = UserId(value = "selfUserId", domain = "selfDomain") @Mock - private val userRepository = mock(UserRepository::class) + private val callRepository = mock(CallRepository::class) @Mock private val messageSender = mock(MessageSender::class) @@ -120,7 +118,6 @@ class CallManagerTest { callManagerImpl = CallManagerImpl( calling = calling, callRepository = callRepository, - userRepository = userRepository, currentClientIdProvider = currentClientIdProvider, selfConversationIdProvider = selfConversationIdProvider, conversationRepository = conversationRepository, @@ -137,7 +134,8 @@ class CallManagerTest { kaliumConfigs = kaliumConfigs, mediaManagerService = mediaManagerService, flowManagerService = flowManagerService, - createAndPersistRecentlyEndedCallMetadata = createAndPersistRecentlyEndedCallMetadata + createAndPersistRecentlyEndedCallMetadata = createAndPersistRecentlyEndedCallMetadata, + selfUserId = selfUserId ) } diff --git a/logic/src/appleMain/kotlin/com/wire/kalium/logic/feature/call/GlobalCallManager.kt b/logic/src/appleMain/kotlin/com/wire/kalium/logic/feature/call/GlobalCallManager.kt index 648c3d755ee..c082eadd622 100644 --- a/logic/src/appleMain/kotlin/com/wire/kalium/logic/feature/call/GlobalCallManager.kt +++ b/logic/src/appleMain/kotlin/com/wire/kalium/logic/feature/call/GlobalCallManager.kt @@ -31,7 +31,6 @@ import com.wire.kalium.logic.data.id.FederatedIdMapper import com.wire.kalium.logic.data.id.QualifiedID import com.wire.kalium.logic.data.id.QualifiedIdMapper import com.wire.kalium.logic.data.user.UserId -import com.wire.kalium.logic.data.user.UserRepository import com.wire.kalium.logic.feature.call.usecase.ConversationClientsInCallUpdater import com.wire.kalium.logic.feature.call.usecase.GetCallConversationTypeProvider import com.wire.kalium.logic.feature.call.usecase.CreateAndPersistRecentlyEndedCallMetadataUseCase @@ -44,7 +43,6 @@ actual class GlobalCallManager { internal actual fun getCallManagerForClient( userId: QualifiedID, callRepository: CallRepository, - userRepository: UserRepository, currentClientIdProvider: CurrentClientIdProvider, selfConversationIdProvider: SelfConversationIdProvider, conversationRepository: ConversationRepository, diff --git a/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/CallManagerImpl.kt b/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/CallManagerImpl.kt index 0285d2491e4..8628c184699 100644 --- a/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/CallManagerImpl.kt +++ b/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/CallManagerImpl.kt @@ -56,7 +56,6 @@ import com.wire.kalium.logic.data.id.QualifiedIdMapper import com.wire.kalium.logic.data.message.Message import com.wire.kalium.logic.data.message.MessageContent import com.wire.kalium.logic.data.user.UserId -import com.wire.kalium.logic.data.user.UserRepository import com.wire.kalium.logic.feature.call.scenario.CallingMessageSender import com.wire.kalium.logic.feature.call.scenario.OnActiveSpeakers import com.wire.kalium.logic.feature.call.scenario.OnAnsweredCall @@ -104,7 +103,6 @@ import java.util.Collections class CallManagerImpl internal constructor( private val calling: Calling, private val callRepository: CallRepository, - private val userRepository: UserRepository, private val currentClientIdProvider: CurrentClientIdProvider, selfConversationIdProvider: SelfConversationIdProvider, private val conversationRepository: ConversationRepository, @@ -121,6 +119,7 @@ class CallManagerImpl internal constructor( private val mediaManagerService: MediaManagerService, private val flowManagerService: FlowManagerService, private val createAndPersistRecentlyEndedCallMetadata: CreateAndPersistRecentlyEndedCallMetadataUseCase, + private val selfUserId: UserId, private val json: Json = Json { ignoreUnknownKeys = true }, private val shouldRemoteMuteChecker: ShouldRemoteMuteChecker = ShouldRemoteMuteCheckerImpl(), private val serverTimeHandler: ServerTimeHandler = ServerTimeHandlerImpl(), @@ -153,11 +152,6 @@ class CallManagerImpl internal constructor( it }) } - private val userId: Deferred = scope.async(start = CoroutineStart.LAZY) { - userRepository.observeSelfUser().first().id.also { - callingLogger.d("$TAG - userId ${it.toLogString()}") - } - } @Suppress("UNUSED_ANONYMOUS_PARAMETER") private val metricsHandler = MetricsHandler { conversationId: String, metricsJson: String, arg: Pointer? -> @@ -186,7 +180,7 @@ class CallManagerImpl internal constructor( } joinAll(flowManagerStartJob, mediaManagerStartJob) callingLogger.i("$TAG: Creating Handle") - val selfUserId = federatedIdMapper.parseToFederatedId(userId.await()) + val selfUserId = federatedIdMapper.parseToFederatedId(selfUserId) val selfClientId = clientId.await().value val waitInitializationJob = Job() @@ -254,7 +248,7 @@ class CallManagerImpl internal constructor( val conversationMembers = conversationRepository.observeConversationMembers(message.conversationId).first() val shouldRemoteMute = shouldRemoteMuteChecker.check( senderUserId = message.senderUserId, - selfUserId = userId.await(), + selfUserId = selfUserId, selfClientId = clientId.await().value, targets = callingValue.targets, conversationMembers = conversationMembers @@ -306,7 +300,7 @@ class CallManagerImpl internal constructor( isMuted = false, isCameraOn = isCameraOn, isCbrEnabled = isAudioCbr, - callerId = userId.await() + callerId = selfUserId ) withCalling { @@ -401,7 +395,7 @@ class CallManagerImpl internal constructor( } callingLogger.d("$TAG -> set test video to $logString") - val selfUserId = federatedIdMapper.parseToFederatedId(userId.await()) + val selfUserId = federatedIdMapper.parseToFederatedId(selfUserId) val selfClientId = clientId.await().value val handle = deferredHandle.await() diff --git a/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/GlobalCallManager.kt b/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/GlobalCallManager.kt index 1471ec8dd5e..b147743f422 100644 --- a/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/GlobalCallManager.kt +++ b/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/GlobalCallManager.kt @@ -36,7 +36,6 @@ import com.wire.kalium.logic.data.id.FederatedIdMapper import com.wire.kalium.logic.data.id.QualifiedID import com.wire.kalium.logic.data.id.QualifiedIdMapper import com.wire.kalium.logic.data.user.UserId -import com.wire.kalium.logic.data.user.UserRepository import com.wire.kalium.logic.feature.call.usecase.ConversationClientsInCallUpdater import com.wire.kalium.logic.feature.call.usecase.GetCallConversationTypeProvider import com.wire.kalium.logic.feature.call.usecase.CreateAndPersistRecentlyEndedCallMetadataUseCase @@ -82,7 +81,6 @@ actual class GlobalCallManager( internal actual fun getCallManagerForClient( userId: QualifiedID, callRepository: CallRepository, - userRepository: UserRepository, currentClientIdProvider: CurrentClientIdProvider, selfConversationIdProvider: SelfConversationIdProvider, conversationRepository: ConversationRepository, @@ -103,7 +101,7 @@ actual class GlobalCallManager( CallManagerImpl( calling = calling, callRepository = callRepository, - userRepository = userRepository, + selfUserId = userId, currentClientIdProvider = currentClientIdProvider, selfConversationIdProvider = selfConversationIdProvider, callMapper = callMapper, diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/call/CallingParticipantsOrder.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/call/CallingParticipantsOrder.kt index 9a160188226..5511f8bcb5b 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/call/CallingParticipantsOrder.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/call/CallingParticipantsOrder.kt @@ -18,8 +18,8 @@ package com.wire.kalium.logic.data.call -import com.wire.kalium.logic.data.user.UserRepository import com.wire.kalium.logic.data.id.CurrentClientIdProvider +import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.functional.fold internal interface CallingParticipantsOrder { @@ -27,10 +27,10 @@ internal interface CallingParticipantsOrder { } internal class CallingParticipantsOrderImpl( - private val userRepository: UserRepository, private val currentClientIdProvider: CurrentClientIdProvider, private val participantsFilter: ParticipantsFilter, private val participantsOrderByName: ParticipantsOrderByName, + private val selfUserId: UserId ) : CallingParticipantsOrder { override suspend fun reorderItems(participants: List): List { @@ -38,7 +38,6 @@ internal class CallingParticipantsOrderImpl( currentClientIdProvider().fold({ participants }, { selfClientId -> - val selfUserId = userRepository.getSelfUser()?.id!! val selfParticipant = participantsFilter.selfParticipant(participants, selfUserId, selfClientId.value) val otherParticipants = participantsFilter.otherParticipants(participants, selfClientId.value) diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/user/UserRepository.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/user/UserRepository.kt index b283f3158b6..51f1549d955 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/user/UserRepository.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/user/UserRepository.kt @@ -231,6 +231,7 @@ internal class UserDataSource internal constructor( private suspend fun updateSelfUserProviderAccountInfo(userDTO: SelfUserDTO): Either = sessionRepository.updateSsoIdAndScimInfo(userDTO.id.toModel(), idMapper.toSsoId(userDTO.ssoID), userDTO.managedByDTO) + // TODO: race condition, if we request the same user (can happen for self) multiple times, we will fetch it multiple times override suspend fun getKnownUser(userId: UserId): Flow = userDAO.observeUserDetailsByQualifiedID(qualifiedID = userId.toDao()) .map { userEntity -> @@ -416,6 +417,7 @@ internal class UserDataSource internal constructor( else fetchUsersByIds(missingIds.map { it.toModel() }.toSet()).map { } } + // TODO: this can cause many issues since it will @OptIn(ExperimentalCoroutinesApi::class) override suspend fun observeSelfUser(): Flow { return metadataDAO.valueByKeyFlow(SELF_USER_ID_KEY).onEach { @@ -469,8 +471,9 @@ internal class UserDataSource internal constructor( } // TODO: replace the flow with selfUser and cache it - override suspend fun getSelfUser(): SelfUser? = - observeSelfUser().firstOrNull() + override suspend fun getSelfUser(): SelfUser? { + return observeSelfUser().firstOrNull() + } override suspend fun observeAllKnownUsers(): Flow>> { val selfUserId = selfUserId.toDao() @@ -658,7 +661,6 @@ internal class UserDataSource internal constructor( CreateUserTeam(dto.teamId, dto.teamName) } .onSuccess { - kaliumLogger.d("Migrated user to team") fetchSelfUser() } .onFailure { failure -> diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/UserSessionScope.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/UserSessionScope.kt index 9f0beaf9225..14bdb75e474 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/UserSessionScope.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/UserSessionScope.kt @@ -1264,7 +1264,6 @@ class UserSessionScope internal constructor( globalCallManager.getCallManagerForClient( userId = userId, callRepository = callRepository, - userRepository = userRepository, currentClientIdProvider = clientIdProvider, conversationRepository = conversationRepository, userConfigRepository = userConfigRepository, @@ -2062,7 +2061,6 @@ class UserSessionScope internal constructor( callManager = callManager, callRepository = callRepository, conversationRepository = conversationRepository, - userRepository = userRepository, flowManagerService = flowManagerService, mediaManagerService = mediaManagerService, syncManager = syncManager, @@ -2073,6 +2071,8 @@ class UserSessionScope internal constructor( conversationClientsInCallUpdater = conversationClientsInCallUpdater, kaliumConfigs = kaliumConfigs, inCallReactionsRepository = inCallReactionsRepository, + selfUserId = userId, + userRepository = userRepository ) val connection: ConnectionScope diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/CallsScope.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/CallsScope.kt index 9066eccd245..b281532fe8c 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/CallsScope.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/CallsScope.kt @@ -28,6 +28,7 @@ import com.wire.kalium.logic.data.call.ParticipantsOrderByNameImpl import com.wire.kalium.logic.data.conversation.ConversationRepository import com.wire.kalium.logic.data.id.CurrentClientIdProvider import com.wire.kalium.logic.data.id.QualifiedIdMapper +import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.data.user.UserRepository import com.wire.kalium.logic.feature.call.usecase.AnswerCallUseCase import com.wire.kalium.logic.feature.call.usecase.AnswerCallUseCaseImpl @@ -107,6 +108,7 @@ class CallsScope internal constructor( private val getCallConversationType: GetCallConversationTypeProvider, private val kaliumConfigs: KaliumConfigs, private val inCallReactionsRepository: InCallReactionsRepository, + private val selfUserId: UserId, internal val dispatcher: KaliumDispatcher = KaliumDispatcherImpl ) { @@ -122,7 +124,7 @@ class CallsScope internal constructor( get() = GetIncomingCallsUseCaseImpl( callRepository = callRepository, conversationRepository = conversationRepository, - userRepository = userRepository + userRepository = userRepository, ) val observeOutgoingCall: ObserveOutgoingCallUseCase get() = ObserveOutgoingCallUseCaseImpl( @@ -211,10 +213,10 @@ class CallsScope internal constructor( private val callingParticipantsOrder: CallingParticipantsOrder get() = CallingParticipantsOrderImpl( - userRepository = userRepository, currentClientIdProvider = currentClientIdProvider, participantsFilter = ParticipantsFilterImpl(qualifiedIdMapper), - participantsOrderByName = ParticipantsOrderByNameImpl() + participantsOrderByName = ParticipantsOrderByNameImpl(), + selfUserId = selfUserId ) val isLastCallClosed: IsLastCallClosedUseCase get() = IsLastCallClosedUseCaseImpl(callRepository) diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/GlobalCallManager.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/GlobalCallManager.kt index 12f8ee9dadf..9622686a76f 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/GlobalCallManager.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/GlobalCallManager.kt @@ -30,7 +30,6 @@ import com.wire.kalium.logic.data.id.FederatedIdMapper import com.wire.kalium.logic.data.id.QualifiedID import com.wire.kalium.logic.data.id.QualifiedIdMapper import com.wire.kalium.logic.data.user.UserId -import com.wire.kalium.logic.data.user.UserRepository import com.wire.kalium.logic.data.id.CurrentClientIdProvider import com.wire.kalium.logic.feature.call.usecase.ConversationClientsInCallUpdater import com.wire.kalium.logic.feature.call.usecase.GetCallConversationTypeProvider @@ -45,7 +44,6 @@ expect class GlobalCallManager { internal fun getCallManagerForClient( userId: QualifiedID, callRepository: CallRepository, - userRepository: UserRepository, currentClientIdProvider: CurrentClientIdProvider, selfConversationIdProvider: SelfConversationIdProvider, conversationRepository: ConversationRepository, diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/usecase/GetIncomingCallsUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/usecase/GetIncomingCallsUseCase.kt index 0aea1f39c30..c442b493540 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/usecase/GetIncomingCallsUseCase.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/usecase/GetIncomingCallsUseCase.kt @@ -70,9 +70,9 @@ internal class GetIncomingCallsUseCaseImpl internal constructor( @OptIn(ExperimentalCoroutinesApi::class) private suspend fun observeIncomingCallsIfUserStatusAllows(): Flow> = + // TODO: do not refresh self form remote in this case and just get status from local userRepository.observeSelfUser() .flatMapLatest { - // if user is AWAY we don't show any IncomingCalls if (it.availabilityStatus == UserAvailabilityStatus.AWAY) { logger.d("$TAG; Ignoring possible calls based user's status") diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ConversationScope.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ConversationScope.kt index 7f73c1c6cda..39839498275 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ConversationScope.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ConversationScope.kt @@ -141,7 +141,7 @@ class ConversationScope internal constructor( get() = ObserveConversationMembersUseCaseImpl(conversationRepository, userRepository) val getMembersToMention: MembersToMentionUseCase - get() = MembersToMentionUseCase(observeConversationMembers, userRepository) + get() = MembersToMentionUseCase(observeConversationMembers = observeConversationMembers, selfUserId = selfUserId) val observeUserListById: ObserveUserListByIdUseCase get() = ObserveUserListByIdUseCase(userRepository) diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/MembersToMentionUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/MembersToMentionUseCase.kt index 8e828c4a8bf..bcb7eacd1cd 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/MembersToMentionUseCase.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/MembersToMentionUseCase.kt @@ -20,7 +20,7 @@ package com.wire.kalium.logic.feature.conversation import com.wire.kalium.logic.data.conversation.MemberDetails import com.wire.kalium.logic.data.id.ConversationId -import com.wire.kalium.logic.data.user.UserRepository +import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.util.KaliumDispatcher import com.wire.kalium.util.KaliumDispatcherImpl import kotlinx.coroutines.flow.first @@ -42,7 +42,7 @@ import kotlinx.coroutines.withContext @Suppress("ReturnCount") class MembersToMentionUseCase internal constructor( private val observeConversationMembers: ObserveConversationMembersUseCase, - private val userRepository: UserRepository, + private val selfUserId: UserId, private val dispatcher: KaliumDispatcher = KaliumDispatcherImpl ) { /** @@ -57,7 +57,7 @@ class MembersToMentionUseCase internal constructor( // TODO apply normalization techniques that are used for other searches to the name (e.g. ö -> oe) val usersToSearch = conversationMembers.filter { - it.user.id != userRepository.getSelfUser()?.id + it.user.id != selfUserId } if (searchQuery.isEmpty()) return@withContext usersToSearch diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/debug/DebugScope.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/debug/DebugScope.kt index 34b31cdf4b7..3ac9e3e5763 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/debug/DebugScope.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/debug/DebugScope.kt @@ -116,10 +116,10 @@ class DebugScope internal constructor( val sendConfirmation: SendConfirmationUseCase get() = SendConfirmationUseCase( - userRepository, - currentClientIdProvider, - slowSyncRepository, - messageSender + currentClientIdProvider = currentClientIdProvider, + slowSyncRepository = slowSyncRepository, + messageSender = messageSender, + selfUserId = userId, ) val disableEventProcessing: DisableEventProcessingUseCase diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/debug/SendConfirmationUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/debug/SendConfirmationUseCase.kt index 8babeb382bc..d59c3f9988d 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/debug/SendConfirmationUseCase.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/debug/SendConfirmationUseCase.kt @@ -26,8 +26,8 @@ import com.wire.kalium.logic.data.message.MessageContent import com.wire.kalium.logic.data.message.receipt.ReceiptType import com.wire.kalium.logic.data.sync.SlowSyncRepository import com.wire.kalium.logic.data.sync.SlowSyncStatus -import com.wire.kalium.logic.data.user.UserRepository import com.wire.kalium.logic.data.id.CurrentClientIdProvider +import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.feature.message.MessageSender import com.wire.kalium.logic.functional.Either import com.wire.kalium.logic.functional.flatMap @@ -41,10 +41,10 @@ import kotlinx.datetime.Clock * client behaviour. It should not be used by clients itself. */ class SendConfirmationUseCase internal constructor( - private val userRepository: UserRepository, private val currentClientIdProvider: CurrentClientIdProvider, private val slowSyncRepository: SlowSyncRepository, - private val messageSender: MessageSender + private val messageSender: MessageSender, + private val selfUserId: UserId ) { suspend operator fun invoke( @@ -57,8 +57,6 @@ class SendConfirmationUseCase internal constructor( it is SlowSyncStatus.Complete } - val selfUser = userRepository.observeSelfUser().first() - val generatedMessageUuid = uuid4().toString() return currentClientIdProvider().flatMap { currentClientId -> @@ -67,7 +65,7 @@ class SendConfirmationUseCase internal constructor( content = MessageContent.Receipt(type, listOf(firstMessageId) + moreMessageIds), conversationId = conversationId, date = Clock.System.now(), - senderUserId = selfUser.id, + senderUserId = selfUserId, senderClientId = currentClientId, status = Message.Status.Pending, isSelfMessage = true, diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/call/CallingParticipantsOrderTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/call/CallingParticipantsOrderTest.kt index ff57b4aba81..df5304046e5 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/call/CallingParticipantsOrderTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/call/CallingParticipantsOrderTest.kt @@ -45,10 +45,6 @@ import kotlin.test.Test import kotlin.test.assertEquals class CallingParticipantsOrderTest { - - @Mock - private val userRepository = mock(UserRepository::class) - @Mock private val participantsFilter = mock(ParticipantsFilter::class) @@ -63,7 +59,12 @@ class CallingParticipantsOrderTest { @BeforeTest fun setup() { callingParticipantsOrder = - CallingParticipantsOrderImpl(userRepository, currentClientIdProvider, participantsFilter, participantsOrderByName) + CallingParticipantsOrderImpl( + currentClientIdProvider, + participantsFilter, + selfUserId = selfUserId, + participantsOrderByName = participantsOrderByName + ) } @Test @@ -93,10 +94,6 @@ class CallingParticipantsOrderTest { currentClientIdProvider.invoke() }.returns(Either.Right(ClientId(selfClientId))) - coEvery { - userRepository.getSelfUser() - }.returns(selfUser) - every { participantsFilter.otherParticipants(participants, selfClientId) }.returns(otherParticipants) diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/MembersToMentionUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/MembersToMentionUseCaseTest.kt index 792a6404da0..d9a3df708f3 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/MembersToMentionUseCaseTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/MembersToMentionUseCaseTest.kt @@ -44,8 +44,7 @@ import kotlin.test.assertEquals class MembersToMentionUseCaseTest { - @Mock - private val userRepository: UserRepository = mock(UserRepository::class) + private val selfUserId = SELF_USER.id @Mock private val observeConversationMembers = mock(ObserveConversationMembersUseCase::class) @@ -54,10 +53,7 @@ class MembersToMentionUseCaseTest { @BeforeTest fun setup() = runBlocking { - membersToMention = MembersToMentionUseCase(observeConversationMembers, userRepository, TestKaliumDispatcher) - coEvery { - userRepository.getSelfUser() - }.returns(SELF_USER) + membersToMention = MembersToMentionUseCase(observeConversationMembers, selfUserId = selfUserId, TestKaliumDispatcher) coEvery { observeConversationMembers.invoke(any()) }.returns(flowOf(members)) From 4362a2300c265ad69d9d86756ce9099953035b17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20=C5=BBerko?= Date: Mon, 13 Jan 2025 13:05:46 +0100 Subject: [PATCH 3/5] feat: move to folder [WPB-14627] (#3213) * feat: move conversation to folder * feat: move to folder use case * refactor and added tests * tests fix --- .../logic/data/conversation/Conversation.kt | 6 +- .../data/conversation/ConversationMapper.kt | 6 +- .../folders/ConversationFolderRepository.kt | 6 +- .../feature/conversation/ConversationScope.kt | 4 + .../folder/MoveConversationToFolderUseCase.kt | 80 ++++++++ .../folder/ObserveUserFoldersUseCase.kt | 12 +- .../MoveConversationToFolderUseCaseTest.kt | 175 ++++++++++++++++++ .../folder/ObserveUserFoldersUseCaseTest.kt | 148 +++++++++++++++ .../logic/framework/TestConversation.kt | 8 +- .../kalium/persistence/ConversationDetails.sq | 10 +- .../kalium/persistence/ConversationFolders.sq | 5 +- .../src/commonMain/db_user/migrations/95.sqm | 131 +++++++++++++ .../ConversationDetailsWithEventsMapper.kt | 6 +- .../dao/conversation/ConversationMapper.kt | 6 +- .../conversation/ConversationViewEntity.kt | 2 + .../folder/ConversationFolderDAO.kt | 2 +- .../folder/ConversationFolderDAOImpl.kt | 4 +- .../persistence/dao/ConversationDAOTest.kt | 4 +- 18 files changed, 593 insertions(+), 22 deletions(-) create mode 100644 logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/folder/MoveConversationToFolderUseCase.kt create mode 100644 logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/folder/MoveConversationToFolderUseCaseTest.kt create mode 100644 logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/folder/ObserveUserFoldersUseCaseTest.kt create mode 100644 persistence/src/commonMain/db_user/migrations/95.sqm diff --git a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/Conversation.kt b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/Conversation.kt index b07cf80f009..20ba492df9f 100644 --- a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/Conversation.kt +++ b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/Conversation.kt @@ -307,7 +307,8 @@ sealed class ConversationDetails(open val conversation: Conversation) { override val conversation: Conversation, val otherUser: OtherUser, val userType: UserType, - val isFavorite: Boolean = false + val isFavorite: Boolean = false, + val folder: ConversationFolder? = null ) : ConversationDetails(conversation) data class Group( @@ -315,7 +316,8 @@ sealed class ConversationDetails(open val conversation: Conversation) { val hasOngoingCall: Boolean = false, val isSelfUserMember: Boolean, val selfRole: Conversation.Member.Role?, - val isFavorite: Boolean = false + val isFavorite: Boolean = false, + val folder: ConversationFolder? = null // val isTeamAdmin: Boolean, TODO kubaz ) : ConversationDetails(conversation) diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationMapper.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationMapper.kt index 8ec1b717167..38c398ef667 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationMapper.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationMapper.kt @@ -263,7 +263,8 @@ internal class ConversationMapperImpl( activeOneOnOneConversationId = userActiveOneOnOneConversationId?.toModel() ), userType = domainUserTypeMapper.fromUserTypeEntity(userType), - isFavorite = isFavorite + isFavorite = isFavorite, + folder = folderId?.let { ConversationFolder(it, folderName ?: "", type = FolderType.USER) }, ) } @@ -273,7 +274,8 @@ internal class ConversationMapperImpl( hasOngoingCall = callStatus != null, // todo: we can do better! isSelfUserMember = isMember, selfRole = selfRole?.let { conversationRoleMapper.fromDAO(it) }, - isFavorite = isFavorite + isFavorite = isFavorite, + folder = folderId?.let { ConversationFolder(it, folderName ?: "", type = FolderType.USER) }, ) } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/folders/ConversationFolderRepository.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/folders/ConversationFolderRepository.kt index 5db72788cbd..d92c71317b8 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/folders/ConversationFolderRepository.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/folders/ConversationFolderRepository.kt @@ -58,7 +58,7 @@ internal interface ConversationFolderRepository { suspend fun addConversationToFolder(conversationId: QualifiedID, folderId: String): Either suspend fun removeConversationFromFolder(conversationId: QualifiedID, folderId: String): Either suspend fun syncConversationFoldersFromLocal(): Either - suspend fun observeUserFolders(): Flow>> + suspend fun observeFolders(): Flow>> } internal class ConversationFolderDataSource internal constructor( @@ -155,8 +155,8 @@ internal class ConversationFolderDataSource internal constructor( } } - override suspend fun observeUserFolders(): Flow>> { - return conversationFolderDAO.observeUserFolders() + override suspend fun observeFolders(): Flow>> { + return conversationFolderDAO.observeFolders() .wrapStorageRequest() .mapRight { folderEntities -> folderEntities.map { it.toModel() } } } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ConversationScope.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ConversationScope.kt index 39839498275..842a6765fc8 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ConversationScope.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ConversationScope.kt @@ -55,6 +55,8 @@ import com.wire.kalium.logic.feature.conversation.folder.AddConversationToFavori import com.wire.kalium.logic.feature.conversation.folder.AddConversationToFavoritesUseCaseImpl import com.wire.kalium.logic.feature.conversation.folder.GetFavoriteFolderUseCase import com.wire.kalium.logic.feature.conversation.folder.GetFavoriteFolderUseCaseImpl +import com.wire.kalium.logic.feature.conversation.folder.MoveConversationToFolderUseCase +import com.wire.kalium.logic.feature.conversation.folder.MoveConversationToFolderUseCaseImpl import com.wire.kalium.logic.feature.conversation.folder.ObserveConversationsFromFolderUseCase import com.wire.kalium.logic.feature.conversation.folder.ObserveConversationsFromFolderUseCaseImpl import com.wire.kalium.logic.feature.conversation.folder.ObserveUserFoldersUseCase @@ -386,4 +388,6 @@ class ConversationScope internal constructor( get() = RemoveConversationFromFavoritesUseCaseImpl(conversationFolderRepository) val observeUserFolders: ObserveUserFoldersUseCase get() = ObserveUserFoldersUseCaseImpl(conversationFolderRepository) + val moveConversationToFolder: MoveConversationToFolderUseCase + get() = MoveConversationToFolderUseCaseImpl(conversationFolderRepository) } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/folder/MoveConversationToFolderUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/folder/MoveConversationToFolderUseCase.kt new file mode 100644 index 00000000000..7ee1e1fbcd7 --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/folder/MoveConversationToFolderUseCase.kt @@ -0,0 +1,80 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +package com.wire.kalium.logic.feature.conversation.folder + +import com.wire.kalium.logic.CoreFailure +import com.wire.kalium.logic.data.conversation.folders.ConversationFolderRepository +import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.functional.Either +import com.wire.kalium.logic.functional.flatMap +import com.wire.kalium.logic.functional.fold +import com.wire.kalium.util.KaliumDispatcher +import com.wire.kalium.util.KaliumDispatcherImpl +import kotlinx.coroutines.withContext + +/** + * This use case will move a conversation to the selected folder. + */ +interface MoveConversationToFolderUseCase { + /** + * @param conversationId the id of the conversation + * @param folderId the id of the conversation folder + * @param previousFolderId the id of the previous folder, if any + * @return the [Result] indicating a successful operation, otherwise a [CoreFailure] + */ + suspend operator fun invoke( + conversationId: ConversationId, + folderId: String, + previousFolderId: String? + ): Result + + sealed interface Result { + data object Success : Result + data class Failure(val cause: CoreFailure) : Result + } +} + +internal class MoveConversationToFolderUseCaseImpl( + private val conversationFolderRepository: ConversationFolderRepository, + private val dispatchers: KaliumDispatcher = KaliumDispatcherImpl +) : MoveConversationToFolderUseCase { + override suspend fun invoke( + conversationId: ConversationId, + folderId: String, + previousFolderId: String? + ): MoveConversationToFolderUseCase.Result = withContext(dispatchers.io) { + ( + previousFolderId?.let { + conversationFolderRepository.removeConversationFromFolder(conversationId, it) + } ?: Either.Right(Unit) + ) + .flatMap { + conversationFolderRepository.addConversationToFolder( + conversationId, + folderId + ) + } + .flatMap { conversationFolderRepository.syncConversationFoldersFromLocal() } + .fold({ + MoveConversationToFolderUseCase.Result.Failure(it) + }, { + MoveConversationToFolderUseCase.Result.Success + }) + } +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/folder/ObserveUserFoldersUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/folder/ObserveUserFoldersUseCase.kt index 3935cd2af28..f0399efbca5 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/folder/ObserveUserFoldersUseCase.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/folder/ObserveUserFoldersUseCase.kt @@ -18,7 +18,9 @@ package com.wire.kalium.logic.feature.conversation.folder import com.wire.kalium.logic.data.conversation.ConversationFolder +import com.wire.kalium.logic.data.conversation.FolderType import com.wire.kalium.logic.data.conversation.folders.ConversationFolderRepository +import com.wire.kalium.logic.functional.mapRight import com.wire.kalium.logic.functional.mapToRightOr import com.wire.kalium.util.KaliumDispatcher import com.wire.kalium.util.KaliumDispatcherImpl @@ -39,7 +41,15 @@ internal class ObserveUserFoldersUseCaseImpl( ) : ObserveUserFoldersUseCase { override suspend operator fun invoke(): Flow> { - return conversationFolderRepository.observeUserFolders() + return conversationFolderRepository.observeFolders() + .mapRight { folders -> + if (folders.isEmpty()) { + conversationFolderRepository.fetchConversationFolders() + emptyList() + } else { + folders.filter { it.type == FolderType.USER } + } + } .mapToRightOr(emptyList()) .flowOn(dispatchers.io) } diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/folder/MoveConversationToFolderUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/folder/MoveConversationToFolderUseCaseTest.kt new file mode 100644 index 00000000000..eec942bab2c --- /dev/null +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/folder/MoveConversationToFolderUseCaseTest.kt @@ -0,0 +1,175 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.feature.conversation.folder + +import com.wire.kalium.logic.CoreFailure +import com.wire.kalium.logic.data.conversation.folders.ConversationFolderRepository +import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.feature.conversation.folder.MoveConversationToFolderUseCase.Result +import com.wire.kalium.logic.framework.TestConversation +import com.wire.kalium.logic.functional.Either +import io.mockative.Mock +import io.mockative.coEvery +import io.mockative.coVerify +import io.mockative.mock +import io.mockative.once +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertIs + +class MoveConversationToFolderUseCaseTest { + + @Test + fun givenValidConversationAndFolder_WhenMoveIsSuccessful_ThenReturnSuccess() = runTest { + val testConversationId = TestConversation.ID + val testFolderId = "test-folder-id" + val previousFolderId = "previous-folder-id" + + val (arrangement, moveConversationUseCase) = Arrangement() + .withRemoveConversationFromFolder(testConversationId, previousFolderId, Either.Right(Unit)) + .withAddConversationToFolder(testConversationId, testFolderId, Either.Right(Unit)) + .withSyncFolders(Either.Right(Unit)) + .arrange() + + val result = moveConversationUseCase(testConversationId, testFolderId, previousFolderId) + + assertIs(result) + + coVerify { + arrangement.conversationFolderRepository.removeConversationFromFolder(testConversationId, previousFolderId) + }.wasInvoked(exactly = once) + + coVerify { + arrangement.conversationFolderRepository.addConversationToFolder(testConversationId, testFolderId) + }.wasInvoked(exactly = once) + + coVerify { + arrangement.conversationFolderRepository.syncConversationFoldersFromLocal() + }.wasInvoked(exactly = once) + } + + @Test + fun givenValidConversationAndFolder_WhenRemoveFails_ThenReturnFailure() = runTest { + val testConversationId = TestConversation.ID + val testFolderId = "test-folder-id" + val previousFolderId = "previous-folder-id" + + val (arrangement, moveConversationUseCase) = Arrangement() + .withRemoveConversationFromFolder(testConversationId, previousFolderId, Either.Left(CoreFailure.Unknown(null))) + .arrange() + + val result = moveConversationUseCase(testConversationId, testFolderId, previousFolderId) + + assertIs(result) + + coVerify { + arrangement.conversationFolderRepository.removeConversationFromFolder(testConversationId, previousFolderId) + }.wasInvoked(exactly = once) + } + + @Test + fun givenValidConversationAndFolder_WhenAddFails_ThenReturnFailure() = runTest { + val testConversationId = TestConversation.ID + val testFolderId = "test-folder-id" + val previousFolderId = "previous-folder-id" + + val (arrangement, moveConversationUseCase) = Arrangement() + .withRemoveConversationFromFolder(testConversationId, previousFolderId, Either.Right(Unit)) + .withAddConversationToFolder(testConversationId, testFolderId, Either.Left(CoreFailure.Unknown(null))) + .arrange() + + val result = moveConversationUseCase(testConversationId, testFolderId, previousFolderId) + + assertIs(result) + + coVerify { + arrangement.conversationFolderRepository.removeConversationFromFolder(testConversationId, previousFolderId) + }.wasInvoked(exactly = once) + + coVerify { + arrangement.conversationFolderRepository.addConversationToFolder(testConversationId, testFolderId) + }.wasInvoked(exactly = once) + } + + @Test + fun givenValidConversationAndFolder_WhenSyncFails_ThenReturnFailure() = runTest { + val testConversationId = TestConversation.ID + val testFolderId = "test-folder-id" + val previousFolderId = "previous-folder-id" + + val (arrangement, moveConversationUseCase) = Arrangement() + .withRemoveConversationFromFolder(testConversationId, previousFolderId, Either.Right(Unit)) + .withAddConversationToFolder(testConversationId, testFolderId, Either.Right(Unit)) + .withSyncFolders(Either.Left(CoreFailure.Unknown(null))) + .arrange() + + val result = moveConversationUseCase(testConversationId, testFolderId, previousFolderId) + + assertIs(result) + + coVerify { + arrangement.conversationFolderRepository.removeConversationFromFolder(testConversationId, previousFolderId) + }.wasInvoked(exactly = once) + + coVerify { + arrangement.conversationFolderRepository.addConversationToFolder(testConversationId, testFolderId) + }.wasInvoked(exactly = once) + + coVerify { + arrangement.conversationFolderRepository.syncConversationFoldersFromLocal() + }.wasInvoked(exactly = once) + } + + private class Arrangement { + @Mock + val conversationFolderRepository = mock(ConversationFolderRepository::class) + + private val moveConversationToFolderUseCase = MoveConversationToFolderUseCaseImpl( + conversationFolderRepository + ) + + suspend fun withRemoveConversationFromFolder( + conversationId: ConversationId, + folderId: String, + either: Either + ) = apply { + coEvery { + conversationFolderRepository.removeConversationFromFolder(conversationId, folderId) + }.returns(either) + } + + suspend fun withAddConversationToFolder( + conversationId: ConversationId, + folderId: String, + either: Either + ) = apply { + coEvery { + conversationFolderRepository.addConversationToFolder(conversationId, folderId) + }.returns(either) + } + + suspend fun withSyncFolders(either: Either) = apply { + coEvery { + conversationFolderRepository.syncConversationFoldersFromLocal() + }.returns(either) + } + + fun arrange(block: Arrangement.() -> Unit = { }) = apply(block).let { this to moveConversationToFolderUseCase } + } +} + diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/folder/ObserveUserFoldersUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/folder/ObserveUserFoldersUseCaseTest.kt new file mode 100644 index 00000000000..690c2d3af32 --- /dev/null +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/folder/ObserveUserFoldersUseCaseTest.kt @@ -0,0 +1,148 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.feature.conversation.folder + +import com.wire.kalium.logic.CoreFailure +import com.wire.kalium.logic.data.conversation.ConversationFolder +import com.wire.kalium.logic.data.conversation.FolderType +import com.wire.kalium.logic.data.conversation.folders.ConversationFolderRepository +import com.wire.kalium.logic.functional.Either +import io.mockative.Mock +import io.mockative.coEvery +import io.mockative.coVerify +import io.mockative.mock +import io.mockative.once +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals + +class ObserveUserFoldersUseCaseTest { + + @Test + fun givenFoldersExist_WhenObserved_ThenReturnUserFolders() = runTest { + val userFolders = listOf( + ConversationFolder("1", "Custom Folder", FolderType.USER), + ConversationFolder("2", "Second Folder", FolderType.USER) + ) + val allFolders = userFolders + ConversationFolder("3", "", FolderType.FAVORITE) + + val (arrangement, observeUserFoldersUseCase) = Arrangement() + .withObserveFolders(flowOf(Either.Right(allFolders))) + .arrange() + + val result = observeUserFoldersUseCase().first() + + assertEquals(userFolders, result) + + coVerify { + arrangement.conversationFolderRepository.observeFolders() + }.wasInvoked(exactly = once) + + coVerify { + arrangement.conversationFolderRepository.fetchConversationFolders() + }.wasNotInvoked() + } + + @Test + fun givenNoFoldersExist_WhenObserved_ThenFetchFoldersAndReturnEmptyList() = runTest { + val (arrangement, observeUserFoldersUseCase) = Arrangement() + .withObserveFolders(flowOf(Either.Right(emptyList()))) + .withFetchConversationFolders(Either.Right(Unit)) + .arrange() + + val result = observeUserFoldersUseCase().first() + + assertEquals(emptyList(), result) + + coVerify { + arrangement.conversationFolderRepository.observeFolders() + }.wasInvoked(exactly = once) + + coVerify { + arrangement.conversationFolderRepository.fetchConversationFolders() + }.wasInvoked(exactly = once) + } + + @Test + fun givenOnlyFavoriteFoldersExist_WhenObserved_ThenDoNotFetchFoldersAndReturnEmptyList() = runTest { + val favoriteFolders = listOf( + ConversationFolder("1", "Favorites", FolderType.FAVORITE), + ) + + val (arrangement, observeUserFoldersUseCase) = Arrangement() + .withObserveFolders(flowOf(Either.Right(favoriteFolders))) + .arrange() + + val result = observeUserFoldersUseCase().first() + + assertEquals(emptyList(), result) + + coVerify { + arrangement.conversationFolderRepository.observeFolders() + }.wasInvoked(exactly = once) + + coVerify { + arrangement.conversationFolderRepository.fetchConversationFolders() + }.wasNotInvoked() + } + + @Test + fun givenErrorInObserveFolders_WhenObserved_ThenReturnEmptyList() = runTest { + val (arrangement, observeUserFoldersUseCase) = Arrangement() + .withObserveFolders(flowOf(Either.Left(CoreFailure.Unknown(null)))) + .arrange() + + val result = observeUserFoldersUseCase().first() + + assertEquals(emptyList(), result) + + coVerify { + arrangement.conversationFolderRepository.observeFolders() + }.wasInvoked(exactly = once) + + coVerify { + arrangement.conversationFolderRepository.fetchConversationFolders() + }.wasNotInvoked() + } + + private class Arrangement { + @Mock + val conversationFolderRepository = mock(ConversationFolderRepository::class) + + private val observeUserFoldersUseCase = ObserveUserFoldersUseCaseImpl( + conversationFolderRepository + ) + + suspend fun withObserveFolders(flow: Flow>>) = apply { + coEvery { + conversationFolderRepository.observeFolders() + }.returns(flow) + } + + suspend fun withFetchConversationFolders(either: Either) = apply { + coEvery { + conversationFolderRepository.fetchConversationFolders() + }.returns(either) + } + + fun arrange(block: Arrangement.() -> Unit = { }) = apply(block).let { this to observeUserFoldersUseCase } + } +} diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/framework/TestConversation.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/framework/TestConversation.kt index 6f36299004d..3193d607bf7 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/framework/TestConversation.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/framework/TestConversation.kt @@ -176,7 +176,9 @@ object TestConversation { userActiveOneOnOneConversationId = null, legalHoldStatus = ConversationEntity.LegalHoldStatus.DISABLED, accentId = null, - isFavorite = false + isFavorite = false, + folderId = null, + folderName = null ) fun one_on_one(convId: ConversationId) = Conversation( @@ -341,7 +343,9 @@ object TestConversation { userActiveOneOnOneConversationId = null, legalHoldStatus = ConversationEntity.LegalHoldStatus.DISABLED, accentId = null, - isFavorite = false + isFavorite = false, + folderId = null, + folderName = null ) val MLS_PROTOCOL_INFO = ProtocolInfo.MLS( diff --git a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/ConversationDetails.sq b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/ConversationDetails.sq index b521bb3c216..ca9da4c0efa 100644 --- a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/ConversationDetails.sq +++ b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/ConversationDetails.sq @@ -105,7 +105,9 @@ CASE END ELSE 0 END AS interactionEnabled, -LabeledConversation.folder_id IS NOT NULL AS isFavorite +LabeledConversation.folder_id IS NOT NULL AS isFavorite, +CurrentFolder.id AS folderId, +CurrentFolder.name AS folderName FROM Conversation LEFT JOIN SelfUser LEFT JOIN Member ON Conversation.qualified_id = Member.conversation @@ -121,8 +123,10 @@ LEFT JOIN Connection ON Connection.qualified_conversation = Conversation.qualifi AND Conversation.type IS 'CONNECTION_PENDING') LEFT JOIN User AS connection_user ON Connection.qualified_to = connection_user.qualified_id LEFT JOIN Call ON Call.id IS (SELECT id FROM Call WHERE Call.conversation_id = Conversation.qualified_id AND Call.status IS 'STILL_ONGOING' ORDER BY created_at DESC LIMIT 1) -LEFT JOIN ConversationFolder AS FavoriteFolder ON FavoriteFolder.folder_type = 'FAVORITE' -LEFT JOIN LabeledConversation ON LabeledConversation.conversation_id = Conversation.qualified_id AND LabeledConversation.folder_id = FavoriteFolder.id; +LEFT JOIN ConversationFolder AS FavoriteFolder ON FavoriteFolder.folder_type IS 'FAVORITE' +LEFT JOIN LabeledConversation ON LabeledConversation.conversation_id = Conversation.qualified_id AND LabeledConversation.folder_id = FavoriteFolder.id +LEFT JOIN LabeledConversation AS ConversationLabel ON ConversationLabel.conversation_id = Conversation.qualified_id AND ConversationLabel.folder_id IS NOT FavoriteFolder.id +LEFT JOIN ConversationFolder AS CurrentFolder ON CurrentFolder.id = ConversationLabel.folder_id AND CurrentFolder.folder_type IS NOT 'FAVORITE'; selectAllConversationDetails: SELECT * FROM ConversationDetails diff --git a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/ConversationFolders.sq b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/ConversationFolders.sq index 38d2c6db4bf..b5b165d38d7 100644 --- a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/ConversationFolders.sq +++ b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/ConversationFolders.sq @@ -17,9 +17,8 @@ CREATE TABLE LabeledConversation ( PRIMARY KEY (folder_id, conversation_id) ); -getUserFolders: -SELECT * FROM ConversationFolder -WHERE folder_type != 'FAVORITE'; +getFolders: +SELECT * FROM ConversationFolder; getAllFoldersWithConversations: SELECT diff --git a/persistence/src/commonMain/db_user/migrations/95.sqm b/persistence/src/commonMain/db_user/migrations/95.sqm new file mode 100644 index 00000000000..1a5eee4e177 --- /dev/null +++ b/persistence/src/commonMain/db_user/migrations/95.sqm @@ -0,0 +1,131 @@ +DROP VIEW IF EXISTS ConversationDetails; + +CREATE VIEW IF NOT EXISTS ConversationDetails AS +SELECT +Conversation.qualified_id AS qualifiedId, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.name + WHEN 'CONNECTION_PENDING' THEN connection_user.name + ELSE Conversation.name +END AS name, +Conversation.type, +Call.status AS callStatus, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.preview_asset_id + WHEN 'CONNECTION_PENDING' THEN connection_user.preview_asset_id +END AS previewAssetId, +Conversation.muted_status AS mutedStatus, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.team + ELSE Conversation.team_id +END AS teamId, +CASE (Conversation.type) + WHEN 'CONNECTION_PENDING' THEN Connection.last_update_date + ELSE Conversation.last_modified_date +END AS lastModifiedDate, +Conversation.last_read_date AS lastReadDate, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.user_availability_status + WHEN 'CONNECTION_PENDING' THEN connection_user.user_availability_status +END AS userAvailabilityStatus, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.user_type + WHEN 'CONNECTION_PENDING' THEN connection_user.user_type +END AS userType, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.bot_service + WHEN 'CONNECTION_PENDING' THEN connection_user.bot_service +END AS botService, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.deleted + WHEN 'CONNECTION_PENDING' THEN connection_user.deleted +END AS userDeleted, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.defederated + WHEN 'CONNECTION_PENDING' THEN connection_user.defederated +END AS userDefederated, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.supported_protocols + WHEN 'CONNECTION_PENDING' THEN connection_user.supported_protocols +END AS userSupportedProtocols, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.connection_status + WHEN 'CONNECTION_PENDING' THEN connection_user.connection_status +END AS connectionStatus, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.qualified_id + WHEN 'CONNECTION_PENDING' THEN connection_user.qualified_id +END AS otherUserId, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.active_one_on_one_conversation_id + WHEN 'CONNECTION_PENDING' THEN connection_user.active_one_on_one_conversation_id +END AS otherUserActiveConversationId, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN coalesce(User.active_one_on_one_conversation_id = Conversation.qualified_id, 0) + ELSE 1 +END AS isActive, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.accent_id + ELSE 0 +END AS accentId, +Conversation.last_notified_date AS lastNotifiedMessageDate, +memberRole. role AS selfRole, +Conversation.protocol, +Conversation.mls_cipher_suite, +Conversation.mls_epoch, +Conversation.mls_group_id, +Conversation.mls_last_keying_material_update_date, +Conversation.mls_group_state, +Conversation.access_list, +Conversation.access_role_list, +Conversation.mls_proposal_timer, +Conversation.muted_time, +Conversation.creator_id, +Conversation.receipt_mode, +Conversation.message_timer, +Conversation.user_message_timer, +Conversation.incomplete_metadata, +Conversation.archived, +Conversation.archived_date_time, +Conversation.verification_status AS mls_verification_status, +Conversation.proteus_verification_status, +Conversation.legal_hold_status, +SelfUser.id AS selfUserId, +CASE + WHEN Conversation.type = 'GROUP' THEN + CASE + WHEN memberRole.role IS NOT NULL THEN 1 + ELSE 0 + END + WHEN Conversation.type = 'ONE_ON_ONE' THEN + CASE + WHEN User.defederated = 1 THEN 0 + WHEN User.deleted = 1 THEN 0 + WHEN User.connection_status = 'BLOCKED' THEN 0 + WHEN Conversation.legal_hold_status = 'DEGRADED' THEN 0 + ELSE 1 + END + ELSE 0 +END AS interactionEnabled, +LabeledConversation.folder_id IS NOT NULL AS isFavorite, +CurrentFolder.id AS folderId, +CurrentFolder.name AS folderName +FROM Conversation +LEFT JOIN SelfUser +LEFT JOIN Member ON Conversation.qualified_id = Member.conversation + AND Conversation.type IS 'ONE_ON_ONE' + AND Member.user IS NOT SelfUser.id +LEFT JOIN Member AS memberRole ON Conversation.qualified_id = memberRole.conversation + AND memberRole.user IS SelfUser.id +LEFT JOIN User ON User.qualified_id = Member.user +LEFT JOIN Connection ON Connection.qualified_conversation = Conversation.qualified_id + AND (Connection.status = 'SENT' + OR Connection.status = 'PENDING' + OR Connection.status = 'NOT_CONNECTED' + AND Conversation.type IS 'CONNECTION_PENDING') +LEFT JOIN User AS connection_user ON Connection.qualified_to = connection_user.qualified_id +LEFT JOIN Call ON Call.id IS (SELECT id FROM Call WHERE Call.conversation_id = Conversation.qualified_id AND Call.status IS 'STILL_ONGOING' ORDER BY created_at DESC LIMIT 1) +LEFT JOIN ConversationFolder AS FavoriteFolder ON FavoriteFolder.folder_type IS 'FAVORITE' +LEFT JOIN LabeledConversation ON LabeledConversation.conversation_id = Conversation.qualified_id AND LabeledConversation.folder_id = FavoriteFolder.id +LEFT JOIN LabeledConversation AS ConversationLabel ON ConversationLabel.conversation_id = Conversation.qualified_id AND ConversationLabel.folder_id IS NOT FavoriteFolder.id +LEFT JOIN ConversationFolder AS CurrentFolder ON CurrentFolder.id = ConversationLabel.folder_id AND CurrentFolder.folder_type IS NOT 'FAVORITE'; diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDetailsWithEventsMapper.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDetailsWithEventsMapper.kt index d59c32c6c2f..d2fdbad9b30 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDetailsWithEventsMapper.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDetailsWithEventsMapper.kt @@ -80,6 +80,8 @@ data object ConversationDetailsWithEventsMapper { selfUserId: QualifiedIDEntity?, interactionEnabled: Long, isFavorite: Boolean, + folderId: String?, + folderName: String?, unreadKnocksCount: Long?, unreadMissedCallsCount: Long?, unreadMentionsCount: Long?, @@ -153,7 +155,9 @@ data object ConversationDetailsWithEventsMapper { legalHoldStatus = legalHoldStatus, selfUserId = selfUserId, interactionEnabled = interactionEnabled, - isFavorite = isFavorite + isFavorite = isFavorite, + folderId = folderId, + folderName = folderName, ), unreadEvents = UnreadEventMapper.toConversationUnreadEntity( conversationId = qualifiedId, diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationMapper.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationMapper.kt index 4cb050db21d..9c87536de48 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationMapper.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationMapper.kt @@ -76,6 +76,8 @@ data object ConversationMapper { selfUserId: QualifiedIDEntity?, interactionEnabled: Long, isFavorite: Boolean, + folderId: String?, + folderName: String?, ): ConversationViewEntity = ConversationViewEntity( id = qualifiedId, name = name, @@ -125,7 +127,9 @@ data object ConversationMapper { proteusVerificationStatus = proteusVerificationStatus, legalHoldStatus = legalHoldStatus, accentId = accentId, - isFavorite = isFavorite + isFavorite = isFavorite, + folderId = folderId, + folderName = folderName ) @Suppress("LongParameterList", "UnusedParameter") diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationViewEntity.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationViewEntity.kt index 8e5e247226e..e6a8fad9306 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationViewEntity.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationViewEntity.kt @@ -74,6 +74,8 @@ data class ConversationViewEntity( val legalHoldStatus: ConversationEntity.LegalHoldStatus, val accentId: Int?, val isFavorite: Boolean, + val folderId: String?, + val folderName: String?, ) { val isMember: Boolean get() = selfRole != null } diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderDAO.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderDAO.kt index 0c201de5b8d..463aae0e637 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderDAO.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderDAO.kt @@ -28,5 +28,5 @@ interface ConversationFolderDAO { suspend fun updateConversationFolders(folderWithConversationsList: List) suspend fun addConversationToFolder(conversationId: QualifiedIDEntity, folderId: String) suspend fun removeConversationFromFolder(conversationId: QualifiedIDEntity, folderId: String) - suspend fun observeUserFolders(): Flow> + suspend fun observeFolders(): Flow> } diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderDAOImpl.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderDAOImpl.kt index 6e703b82419..7bd01e0e57b 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderDAOImpl.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderDAOImpl.kt @@ -37,8 +37,8 @@ class ConversationFolderDAOImpl internal constructor( ) : ConversationFolderDAO { private val conversationDetailsWithEventsMapper = ConversationDetailsWithEventsMapper - override suspend fun observeUserFolders(): Flow> { - return conversationFoldersQueries.getUserFolders() + override suspend fun observeFolders(): Flow> { + return conversationFoldersQueries.getFolders() .asFlow() .mapToList() .map { it.map(::toEntity) } diff --git a/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/ConversationDAOTest.kt b/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/ConversationDAOTest.kt index a000851a0d6..b8134bc780b 100644 --- a/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/ConversationDAOTest.kt +++ b/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/ConversationDAOTest.kt @@ -2369,7 +2369,9 @@ class ConversationDAOTest : BaseDatabaseTest() { userActiveOneOnOneConversationId = null, legalHoldStatus = ConversationEntity.LegalHoldStatus.DISABLED, accentId = 1, - isFavorite = false + isFavorite = false, + folderName = null, + folderId = null ) } From 2a6796aed3f4ee0b67d8fccfd892e57831636232 Mon Sep 17 00:00:00 2001 From: Mohamad Jaara Date: Wed, 15 Jan 2025 12:17:18 +0100 Subject: [PATCH 4/5] feat: database logger [WPB-14608] (#3227) * feat: database logger * detekt * detekt * set logger name per platform * Trigger CI * fix test --- .../feature/debug/ChangeProfilingUseCase.kt | 7 +- .../kalium/logic/feature/debug/DebugScope.kt | 6 +- .../ObserveDatabaseLoggerStateUseCase.kt | 30 +++++ .../persistence/db/DebugExtension.android.kt | 20 ++++ .../kalium/persistence/db/UserDatabase.kt | 1 - .../persistence/db/DebugExtension.apple.kt | 20 ++++ .../kalium/persistence/db/DebugExtension.kt | 111 ++++++++++++++++++ .../persistence/db/UserDatabaseBuilder.kt | 27 ++--- .../persistence/dao/client/ClientDAOTest.kt | 38 ------ .../persistence/db/DebugExtension.js.kt | 22 ++++ .../persistence/db/DebugExtension.jvm.kt | 22 ++++ 11 files changed, 239 insertions(+), 65 deletions(-) create mode 100644 logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/debug/ObserveDatabaseLoggerStateUseCase.kt create mode 100644 persistence/src/androidMain/kotlin/com/wire/kalium/persistence/db/DebugExtension.android.kt create mode 100644 persistence/src/appleMain/kotlin/com/wire/kalium/persistence/db/DebugExtension.apple.kt create mode 100644 persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/DebugExtension.kt create mode 100644 persistence/src/jsMain/kotlin/com/wire/kalium/persistence/db/DebugExtension.js.kt create mode 100644 persistence/src/jvmMain/kotlin/com/wire/kalium/persistence/db/DebugExtension.jvm.kt diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/debug/ChangeProfilingUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/debug/ChangeProfilingUseCase.kt index 83588fea472..fc5ee605a03 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/debug/ChangeProfilingUseCase.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/debug/ChangeProfilingUseCase.kt @@ -23,10 +23,9 @@ class ChangeProfilingUseCase( private val userStorage: UserStorage, ) { /** - * Changes the profiling of the database (cipher_profile) if the profile is specified and the database is encrypted - * @param enabled true to enable profiling, false to disable + * Change profiling state. */ - operator fun invoke(enabled: Boolean) { - userStorage.database.changeProfiling(enabled) + suspend operator fun invoke(enabled: Boolean) { + userStorage.database.debugExtension.changeProfiling(enabled) } } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/debug/DebugScope.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/debug/DebugScope.kt index 3ac9e3e5763..6367488ad5f 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/debug/DebugScope.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/debug/DebugScope.kt @@ -93,7 +93,7 @@ class DebugScope internal constructor( private val legalHoldHandler: LegalHoldHandler, private val notificationTokenRepository: NotificationTokenRepository, private val scope: CoroutineScope, - userStorage: UserStorage, + private val userStorage: UserStorage, logger: KaliumLogger, internal val dispatcher: KaliumDispatcher = KaliumDispatcherImpl, ) { @@ -227,5 +227,7 @@ class DebugScope internal constructor( notificationTokenRepository, ) - val changeProfiling: ChangeProfilingUseCase = ChangeProfilingUseCase(userStorage) + val changeProfiling: ChangeProfilingUseCase get() = ChangeProfilingUseCase(userStorage) + + val observeDatabaseLoggerState get() = ObserveDatabaseLoggerStateUseCase(userStorage) } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/debug/ObserveDatabaseLoggerStateUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/debug/ObserveDatabaseLoggerStateUseCase.kt new file mode 100644 index 00000000000..c4fd40af94d --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/debug/ObserveDatabaseLoggerStateUseCase.kt @@ -0,0 +1,30 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.feature.debug + +import com.wire.kalium.logic.di.UserStorage +import kotlinx.coroutines.flow.Flow + +/** + * Use case to observe the state of the database logger. + */ +class ObserveDatabaseLoggerStateUseCase( + private val userStorage: UserStorage, +) { + suspend operator fun invoke(): Flow = userStorage.database.debugExtension.observeIsProfilingEnabled() +} diff --git a/persistence/src/androidMain/kotlin/com/wire/kalium/persistence/db/DebugExtension.android.kt b/persistence/src/androidMain/kotlin/com/wire/kalium/persistence/db/DebugExtension.android.kt new file mode 100644 index 00000000000..86825694070 --- /dev/null +++ b/persistence/src/androidMain/kotlin/com/wire/kalium/persistence/db/DebugExtension.android.kt @@ -0,0 +1,20 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.persistence.db + +internal actual fun platformDatabaseLogger(): String = "logcat" diff --git a/persistence/src/androidMain/kotlin/com/wire/kalium/persistence/db/UserDatabase.kt b/persistence/src/androidMain/kotlin/com/wire/kalium/persistence/db/UserDatabase.kt index 4491d1bf47c..84f63d0a743 100644 --- a/persistence/src/androidMain/kotlin/com/wire/kalium/persistence/db/UserDatabase.kt +++ b/persistence/src/androidMain/kotlin/com/wire/kalium/persistence/db/UserDatabase.kt @@ -57,7 +57,6 @@ actual fun userDatabaseBuilder( dispatcher = dispatcher, platformDatabaseData = platformDatabaseData, isEncrypted = isEncryptionEnabled, - cipherProfile = "logcat", ) } diff --git a/persistence/src/appleMain/kotlin/com/wire/kalium/persistence/db/DebugExtension.apple.kt b/persistence/src/appleMain/kotlin/com/wire/kalium/persistence/db/DebugExtension.apple.kt new file mode 100644 index 00000000000..0d5faf02c18 --- /dev/null +++ b/persistence/src/appleMain/kotlin/com/wire/kalium/persistence/db/DebugExtension.apple.kt @@ -0,0 +1,20 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.persistence.db + +internal actual fun platformDatabaseLogger(): String = "os_log" diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/DebugExtension.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/DebugExtension.kt new file mode 100644 index 00000000000..a2a232ecd3c --- /dev/null +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/DebugExtension.kt @@ -0,0 +1,111 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.persistence.db + +import app.cash.sqldelight.db.QueryResult +import app.cash.sqldelight.db.SqlDriver +import com.wire.kalium.persistence.dao.MetadataDAO +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +class DebugExtension( + private val sqlDriver: SqlDriver, + private val isEncrypted: Boolean, + private val metaDataDao: MetadataDAO, +) { + + suspend fun observeIsProfilingEnabled(): Flow = + metaDataDao.valueByKeyFlow(KEY_CIPHER_PROFILE) + .map { state -> + state?.let { DBProfile.fromString(it) }.let { + it is DBProfile.ON + } + } + + /** + * Changes the profiling of the database (cipher_profile) if the profile is specified and the database is encrypted + * @param enabled true to enable profiling, false to disable + */ + suspend fun changeProfiling(enabled: Boolean): Long? = + if (isEncrypted) { + val state = if (enabled) DBProfile.ON.Device else DBProfile.Off + sqlDriver.executeQuery( + identifier = null, + sql = """PRAGMA cipher_profile= '${state.logTarget}';""", + mapper = { cursor -> + cursor.next() + cursor.getLong(0).let { QueryResult.Value(it) } + }, + parameters = 0, + ).value.also { + updateMetadata(state) + } + + } else { + error("Cannot change profiling on unencrypted database") + } + + private suspend fun updateMetadata(state: DBProfile) { + metaDataDao.insertValue( + value = state.logTarget, + key = KEY_CIPHER_PROFILE + ) + } + + private companion object { + const val KEY_CIPHER_PROFILE = "cipher_profile" + } +} + +sealed interface DBProfile { + val logTarget: String + + data object Off : DBProfile { + override val logTarget: String = "off" + + override fun toString(): String { + return "off" + } + } + + sealed interface ON : DBProfile { + data object Device : ON { + override val logTarget: String = "logcat" + + override fun toString(): String { + return platformDatabaseLogger() + } + } + + data class CustomFile(override val logTarget: String) : ON { + override fun toString(): String { + return logTarget + } + } + } + + companion object { + fun fromString(value: String): DBProfile = when (value) { + "off" -> Off + platformDatabaseLogger() -> ON.Device + else -> ON.CustomFile(value) + } + } +} + +internal expect fun platformDatabaseLogger(): String diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/UserDatabaseBuilder.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/UserDatabaseBuilder.kt index 76f9ab33e17..7bbebca1f26 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/UserDatabaseBuilder.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/UserDatabaseBuilder.kt @@ -120,7 +120,6 @@ class UserDatabaseBuilder internal constructor( private val platformDatabaseData: PlatformDatabaseData, private val isEncrypted: Boolean, private val queriesContext: CoroutineContext = KaliumDispatcherImpl.io, - private val cipherProfile: String? = null, ) { internal val database: UserDatabase = UserDatabase( @@ -314,30 +313,18 @@ class UserDatabaseBuilder internal constructor( queriesContext ) + val debugExtension: DebugExtension + get() = DebugExtension( + sqlDriver = sqlDriver, + metaDataDao = metadataDAO, + isEncrypted = isEncrypted + ) + /** * @return the absolute path of the DB file or null if the DB file does not exist */ fun dbFileLocation(): String? = getDatabaseAbsoluteFileLocation(platformDatabaseData, userId) - /** - * Changes the profiling of the database (cipher_profile) if the profile is specified and the database is encrypted - * @param enabled true to enable profiling, false to disable - */ - fun changeProfiling(enabled: Boolean) { - if (isEncrypted && cipherProfile != null) { - val cipherProfileValue = if (enabled) cipherProfile else "off" - sqlDriver.executeQuery( - identifier = null, - sql = "PRAGMA cipher_profile='$cipherProfileValue'", - mapper = { - it.next() - it.getLong(0).let { QueryResult.Value(it) } - }, - parameters = 0, - ) - } - } - /** * drops DB connection and delete the DB file */ diff --git a/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/client/ClientDAOTest.kt b/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/client/ClientDAOTest.kt index 4fa472f3ee1..1f871df56f9 100644 --- a/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/client/ClientDAOTest.kt +++ b/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/client/ClientDAOTest.kt @@ -484,44 +484,6 @@ class ClientDAOTest : BaseDatabaseTest() { assertNull(clientDAO.isMLSCapable(userId, clientId = client.id)) } - @Test - fun givenPersistedClient_whenUpsertingTheSameExactClient_thenItShouldIgnoreAndNotNotifyOtherQueries() = runTest { - // Given - userDAO.upsertUser(user) - clientDAO.insertClient(insertedClient) - - clientDAO.observeClient(user.id, insertedClient.id).test { - val initialValue = awaitItem() - assertEquals(insertedClient.toClient(), initialValue) - - // When - clientDAO.insertClient(insertedClient) // the same exact client is being saved again - - // Then - expectNoEvents() // other query should not be notified - } - } - - @Test - fun givenPersistedClient_whenUpsertingUpdatedClient_thenItShouldBeSavedAndOtherQueriesShouldBeUpdated() = runTest { - // Given - userDAO.upsertUser(user) - clientDAO.insertClient(insertedClient) - val updatedInsertedClient = insertedClient.copy(label = "new_label") - - clientDAO.observeClient(user.id, insertedClient.id).test { - val initialValue = awaitItem() - assertEquals(insertedClient.toClient(), initialValue) - - // When - clientDAO.insertClient(updatedInsertedClient) // updated client is being saved that should replace the old one - - // Then - val updatedValue = awaitItem() // other query should be notified - assertEquals(updatedInsertedClient.toClient(), updatedValue) - } - } - private companion object { val userId = QualifiedIDEntity("test", "domain") val user = newUserEntity(userId) diff --git a/persistence/src/jsMain/kotlin/com/wire/kalium/persistence/db/DebugExtension.js.kt b/persistence/src/jsMain/kotlin/com/wire/kalium/persistence/db/DebugExtension.js.kt new file mode 100644 index 00000000000..232bf463761 --- /dev/null +++ b/persistence/src/jsMain/kotlin/com/wire/kalium/persistence/db/DebugExtension.js.kt @@ -0,0 +1,22 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.persistence.db + +internal actual fun platformDatabaseLogger(): String { + TODO("Not yet implemented") +} diff --git a/persistence/src/jvmMain/kotlin/com/wire/kalium/persistence/db/DebugExtension.jvm.kt b/persistence/src/jvmMain/kotlin/com/wire/kalium/persistence/db/DebugExtension.jvm.kt new file mode 100644 index 00000000000..232bf463761 --- /dev/null +++ b/persistence/src/jvmMain/kotlin/com/wire/kalium/persistence/db/DebugExtension.jvm.kt @@ -0,0 +1,22 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.persistence.db + +internal actual fun platformDatabaseLogger(): String { + TODO("Not yet implemented") +} From b304f302a20874093809dea89e03016515d7c9fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20=C5=BBerko?= Date: Wed, 15 Jan 2025 14:20:26 +0100 Subject: [PATCH 5/5] feat: remove conversation from folder [WPB-14630] (#3230) * feat: remove conversation from folder * remove pragma key off * drop instead of move labeled conversations --- .../folders/ConversationFolderRepository.kt | 5 + .../feature/conversation/ConversationScope.kt | 4 + .../RemoveConversationFromFolderUseCase.kt | 74 +++++++ .../ConversationFolderRepositoryTest.kt | 19 ++ ...RemoveConversationFromFolderUseCaseTest.kt | 180 ++++++++++++++++++ .../kalium/persistence/ConversationFolders.sq | 4 +- .../src/commonMain/db_user/migrations/96.sqm | 10 + .../folder/ConversationFolderDAO.kt | 1 + .../folder/ConversationFolderDAOImpl.kt | 10 + .../folder/ConversationFolderEntity.kt | 5 + .../folder/ConversationFolderDAOTest.kt | 76 ++++++++ 11 files changed, 387 insertions(+), 1 deletion(-) create mode 100644 logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/folder/RemoveConversationFromFolderUseCase.kt create mode 100644 logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/folder/RemoveConversationFromFolderUseCaseTest.kt create mode 100644 persistence/src/commonMain/db_user/migrations/96.sqm diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/folders/ConversationFolderRepository.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/folders/ConversationFolderRepository.kt index d92c71317b8..74eb3c227fb 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/folders/ConversationFolderRepository.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/folders/ConversationFolderRepository.kt @@ -57,6 +57,7 @@ internal interface ConversationFolderRepository { suspend fun fetchConversationFolders(): Either suspend fun addConversationToFolder(conversationId: QualifiedID, folderId: String): Either suspend fun removeConversationFromFolder(conversationId: QualifiedID, folderId: String): Either + suspend fun removeFolder(folderId: String): Either suspend fun syncConversationFoldersFromLocal(): Either suspend fun observeFolders(): Flow>> } @@ -143,6 +144,10 @@ internal class ConversationFolderDataSource internal constructor( } } + override suspend fun removeFolder(folderId: String): Either = wrapStorageRequest { + conversationFolderDAO.removeFolder(folderId) + } + override suspend fun syncConversationFoldersFromLocal(): Either { kaliumLogger.withFeatureId(CONVERSATIONS_FOLDERS).v("Syncing conversation folders from local") return wrapStorageRequest { conversationFolderDAO.getFoldersWithConversations().map { it.toModel() } } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ConversationScope.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ConversationScope.kt index 842a6765fc8..6f4256b7ae5 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ConversationScope.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ConversationScope.kt @@ -63,6 +63,8 @@ import com.wire.kalium.logic.feature.conversation.folder.ObserveUserFoldersUseCa import com.wire.kalium.logic.feature.conversation.folder.ObserveUserFoldersUseCaseImpl import com.wire.kalium.logic.feature.conversation.folder.RemoveConversationFromFavoritesUseCase import com.wire.kalium.logic.feature.conversation.folder.RemoveConversationFromFavoritesUseCaseImpl +import com.wire.kalium.logic.feature.conversation.folder.RemoveConversationFromFolderUseCase +import com.wire.kalium.logic.feature.conversation.folder.RemoveConversationFromFolderUseCaseImpl import com.wire.kalium.logic.feature.conversation.guestroomlink.CanCreatePasswordProtectedLinksUseCase import com.wire.kalium.logic.feature.conversation.guestroomlink.GenerateGuestRoomLinkUseCase import com.wire.kalium.logic.feature.conversation.guestroomlink.GenerateGuestRoomLinkUseCaseImpl @@ -390,4 +392,6 @@ class ConversationScope internal constructor( get() = ObserveUserFoldersUseCaseImpl(conversationFolderRepository) val moveConversationToFolder: MoveConversationToFolderUseCase get() = MoveConversationToFolderUseCaseImpl(conversationFolderRepository) + val removeConversationFromFolder: RemoveConversationFromFolderUseCase + get() = RemoveConversationFromFolderUseCaseImpl(conversationFolderRepository) } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/folder/RemoveConversationFromFolderUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/folder/RemoveConversationFromFolderUseCase.kt new file mode 100644 index 00000000000..b1a1f36dffc --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/folder/RemoveConversationFromFolderUseCase.kt @@ -0,0 +1,74 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +package com.wire.kalium.logic.feature.conversation.folder + +import com.wire.kalium.logic.CoreFailure +import com.wire.kalium.logic.data.conversation.folders.ConversationFolderRepository +import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.functional.Either +import com.wire.kalium.logic.functional.flatMap +import com.wire.kalium.logic.functional.fold +import com.wire.kalium.util.KaliumDispatcher +import com.wire.kalium.util.KaliumDispatcherImpl +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.withContext + +/** + * This use case will remove a conversation from the selected folder and if the folder is empty, it will remove the folder. + */ +interface RemoveConversationFromFolderUseCase { + /** + * @param conversationId the id of the conversation + * @param folderId the id of the folder + * @return the [Result] indicating a successful operation, otherwise a [CoreFailure] + */ + suspend operator fun invoke(conversationId: ConversationId, folderId: String): Result + + sealed interface Result { + data object Success : Result + data class Failure(val cause: CoreFailure) : Result + } +} + +internal class RemoveConversationFromFolderUseCaseImpl( + private val conversationFolderRepository: ConversationFolderRepository, + private val dispatchers: KaliumDispatcher = KaliumDispatcherImpl +) : RemoveConversationFromFolderUseCase { + override suspend fun invoke( + conversationId: ConversationId, + folderId: String + ): RemoveConversationFromFolderUseCase.Result = withContext(dispatchers.io) { + conversationFolderRepository.removeConversationFromFolder(conversationId, folderId) + .flatMap { + if (conversationFolderRepository.observeConversationsFromFolder(folderId).first().isEmpty()) { + conversationFolderRepository.removeFolder(folderId) + } else { + Either.Right(Unit) + } + } + .flatMap { + conversationFolderRepository.syncConversationFoldersFromLocal() + } + .fold({ + RemoveConversationFromFolderUseCase.Result.Failure(it) + }, { + RemoveConversationFromFolderUseCase.Result.Success + }) + } +} diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/folders/ConversationFolderRepositoryTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/folders/ConversationFolderRepositoryTest.kt index 77407d39b67..2fadbc3e3fe 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/folders/ConversationFolderRepositoryTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/folders/ConversationFolderRepositoryTest.kt @@ -216,6 +216,20 @@ class ConversationFolderRepositoryTest { coVerify { arrangement.conversationFolderDAO.getFoldersWithConversations() }.wasInvoked() } + @Test + fun givenValidFolderIdWhenRemovingFolderThenShouldRemoveSuccessfully() = runTest { + // given + val folderId = "folder1" + val arrangement = Arrangement().withSuccessfulFolderRemoval() + + // when + val result = arrangement.repository.removeFolder(folderId) + + // then + result.shouldSucceed() + coVerify { arrangement.conversationFolderDAO.removeFolder(eq(folderId)) }.wasInvoked() + } + private class Arrangement { @Mock @@ -278,5 +292,10 @@ class ConversationFolderRepositoryTest { coEvery { conversationFolderDAO.removeConversationFromFolder(any(), any()) }.returns(Unit) return this } + + suspend fun withSuccessfulFolderRemoval(): Arrangement { + coEvery { conversationFolderDAO.removeFolder(any()) }.returns(Unit) + return this + } } } diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/folder/RemoveConversationFromFolderUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/folder/RemoveConversationFromFolderUseCaseTest.kt new file mode 100644 index 00000000000..9f9d290c498 --- /dev/null +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/folder/RemoveConversationFromFolderUseCaseTest.kt @@ -0,0 +1,180 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.feature.conversation.folder + +import com.wire.kalium.logic.CoreFailure +import com.wire.kalium.logic.data.conversation.ConversationDetailsWithEvents +import com.wire.kalium.logic.data.conversation.folders.ConversationFolderRepository +import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.feature.conversation.folder.RemoveConversationFromFolderUseCase.Result +import com.wire.kalium.logic.framework.TestConversationDetails +import com.wire.kalium.logic.functional.Either +import io.mockative.Mock +import io.mockative.coEvery +import io.mockative.coVerify +import io.mockative.mock +import io.mockative.once +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertIs + +class RemoveConversationFromFolderUseCaseTest { + + @Test + fun givenValidConversationAndFolder_WhenRemoveAndSyncSuccessful_ThenReturnSuccess() = runTest { + val testConversationId = ConversationId("conversation-value", "conversation-domain") + val testFolderId = "test-folder-id" + + val (arrangement, removeConversationUseCase) = Arrangement() + .withRemoveConversationFromFolder(testConversationId, testFolderId, Either.Right(Unit)) + .withObserveConversationsFromFolder(testFolderId, flowOf(emptyList())) + .withRemoveFolder(testFolderId, Either.Right(Unit)) + .withSyncFolders(Either.Right(Unit)) + .arrange() + + val result = removeConversationUseCase(testConversationId, testFolderId) + + assertIs(result) + + coVerify { + arrangement.conversationFolderRepository.removeConversationFromFolder(testConversationId, testFolderId) + }.wasInvoked(exactly = once) + + coVerify { + arrangement.conversationFolderRepository.observeConversationsFromFolder(testFolderId) + }.wasInvoked(exactly = once) + + coVerify { + arrangement.conversationFolderRepository.removeFolder(testFolderId) + }.wasInvoked(exactly = once) + + coVerify { + arrangement.conversationFolderRepository.syncConversationFoldersFromLocal() + }.wasInvoked(exactly = once) + } + + @Test + fun givenFolderNotEmpty_WhenRemoveAndSyncSuccessful_ThenReturnSuccessWithoutFolderRemoval() = runTest { + val testConversationId = ConversationId("conversation-value", "conversation-domain") + val testFolderId = "test-folder-id" + + val (arrangement, removeConversationUseCase) = Arrangement() + .withRemoveConversationFromFolder(testConversationId, testFolderId, Either.Right(Unit)) + .withObserveConversationsFromFolder( + testFolderId, + flowOf(listOf(ConversationDetailsWithEvents(TestConversationDetails.CONVERSATION_GROUP))) + ) + .withSyncFolders(Either.Right(Unit)) + .arrange() + + val result = removeConversationUseCase(testConversationId, testFolderId) + + assertIs(result) + + coVerify { + arrangement.conversationFolderRepository.removeConversationFromFolder(testConversationId, testFolderId) + }.wasInvoked(exactly = once) + + coVerify { + arrangement.conversationFolderRepository.observeConversationsFromFolder(testFolderId) + }.wasInvoked(exactly = once) + + coVerify { + arrangement.conversationFolderRepository.removeFolder(testFolderId) + }.wasNotInvoked() + + coVerify { + arrangement.conversationFolderRepository.syncConversationFoldersFromLocal() + }.wasInvoked(exactly = once) + } + + @Test + fun givenErrorDuringFolderRemoval_WhenObservedEmpty_ThenReturnFailure() = runTest { + val testConversationId = ConversationId("conversation-value", "conversation-domain") + val testFolderId = "test-folder-id" + + val (arrangement, removeConversationUseCase) = Arrangement() + .withRemoveConversationFromFolder(testConversationId, testFolderId, Either.Right(Unit)) + .withObserveConversationsFromFolder(testFolderId, flowOf(emptyList())) + .withRemoveFolder(testFolderId, Either.Left(CoreFailure.Unknown(null))) + .arrange() + + val result = removeConversationUseCase(testConversationId, testFolderId) + + assertIs(result) + + coVerify { + arrangement.conversationFolderRepository.removeConversationFromFolder(testConversationId, testFolderId) + }.wasInvoked(exactly = once) + + coVerify { + arrangement.conversationFolderRepository.observeConversationsFromFolder(testFolderId) + }.wasInvoked(exactly = once) + + coVerify { + arrangement.conversationFolderRepository.removeFolder(testFolderId) + }.wasInvoked(exactly = once) + } + + private class Arrangement { + @Mock + val conversationFolderRepository = mock(ConversationFolderRepository::class) + + private val removeConversationFromFolderUseCase = RemoveConversationFromFolderUseCaseImpl( + conversationFolderRepository + ) + + suspend fun withRemoveConversationFromFolder( + conversationId: ConversationId, + folderId: String, + either: Either + ) = apply { + coEvery { + conversationFolderRepository.removeConversationFromFolder(conversationId, folderId) + }.returns(either) + } + + suspend fun withObserveConversationsFromFolder( + folderId: String, + flow: Flow> + ) = apply { + coEvery { + conversationFolderRepository.observeConversationsFromFolder(folderId) + }.returns(flow) + } + + suspend fun withRemoveFolder( + folderId: String, + either: Either + ) = apply { + coEvery { + conversationFolderRepository.removeFolder(folderId) + }.returns(either) + } + + suspend fun withSyncFolders(either: Either) = apply { + coEvery { + conversationFolderRepository.syncConversationFoldersFromLocal() + }.returns(either) + } + + fun arrange(block: Arrangement.() -> Unit = { }) = apply(block).let { this to removeConversationFromFolderUseCase } + } +} diff --git a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/ConversationFolders.sq b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/ConversationFolders.sq index b5b165d38d7..1ebe1a85260 100644 --- a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/ConversationFolders.sq +++ b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/ConversationFolders.sq @@ -11,7 +11,6 @@ CREATE TABLE LabeledConversation ( conversation_id TEXT AS QualifiedIDEntity NOT NULL, folder_id TEXT NOT NULL, - FOREIGN KEY (conversation_id) REFERENCES Conversation(qualified_id) ON DELETE CASCADE ON UPDATE CASCADE, FOREIGN KEY (folder_id) REFERENCES ConversationFolder(id) ON DELETE CASCADE ON UPDATE CASCADE, PRIMARY KEY (folder_id, conversation_id) @@ -68,3 +67,6 @@ DELETE FROM LabeledConversation; clearFolders: DELETE FROM ConversationFolder; + +deleteFolder: +DELETE FROM ConversationFolder WHERE id = ?; diff --git a/persistence/src/commonMain/db_user/migrations/96.sqm b/persistence/src/commonMain/db_user/migrations/96.sqm new file mode 100644 index 00000000000..ad012d44dd2 --- /dev/null +++ b/persistence/src/commonMain/db_user/migrations/96.sqm @@ -0,0 +1,10 @@ +DROP TABLE LabeledConversation; + +CREATE TABLE LabeledConversation ( + conversation_id TEXT AS QualifiedIDEntity NOT NULL, + folder_id TEXT NOT NULL, + + FOREIGN KEY (folder_id) REFERENCES ConversationFolder(id) ON DELETE CASCADE ON UPDATE CASCADE, + + PRIMARY KEY (folder_id, conversation_id) +); diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderDAO.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderDAO.kt index 463aae0e637..c936fed9da3 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderDAO.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderDAO.kt @@ -29,4 +29,5 @@ interface ConversationFolderDAO { suspend fun addConversationToFolder(conversationId: QualifiedIDEntity, folderId: String) suspend fun removeConversationFromFolder(conversationId: QualifiedIDEntity, folderId: String) suspend fun observeFolders(): Flow> + suspend fun removeFolder(folderId: String) } diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderDAOImpl.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderDAOImpl.kt index 7bd01e0e57b..1f71b24e653 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderDAOImpl.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderDAOImpl.kt @@ -21,6 +21,7 @@ import app.cash.sqldelight.coroutines.asFlow import com.wire.kalium.persistence.ConversationFolder import com.wire.kalium.persistence.ConversationFoldersQueries import com.wire.kalium.persistence.GetAllFoldersWithConversations +import com.wire.kalium.persistence.LabeledConversation import com.wire.kalium.persistence.dao.QualifiedIDEntity import com.wire.kalium.persistence.dao.conversation.ConversationDetailsWithEventsEntity import com.wire.kalium.persistence.dao.conversation.ConversationDetailsWithEventsMapper @@ -45,6 +46,10 @@ class ConversationFolderDAOImpl internal constructor( .flowOn(coroutineContext) } + override suspend fun removeFolder(folderId: String) = withContext(coroutineContext) { + conversationFoldersQueries.deleteFolder(folderId) + } + override suspend fun getFoldersWithConversations(): List = withContext(coroutineContext) { val labeledConversationList = conversationFoldersQueries.getAllFoldersWithConversations().executeAsList().map(::toEntity) @@ -69,6 +74,11 @@ class ConversationFolderDAOImpl internal constructor( conversationId = row.conversation_id ) + private fun toEntity(row: LabeledConversation) = ConversationLabelEntity( + folderId = row.folder_id, + conversationId = row.conversation_id + ) + private fun toEntity(row: ConversationFolder) = ConversationFolderEntity( id = row.id, name = row.name, diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderEntity.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderEntity.kt index 77ceccda8f5..9c1b6a38582 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderEntity.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderEntity.kt @@ -39,6 +39,11 @@ data class LabeledConversationEntity( val conversationId: QualifiedIDEntity? ) +data class ConversationLabelEntity( + val conversationId: QualifiedIDEntity, + val folderId: String +) + enum class ConversationFolderTypeEntity { USER, FAVORITE diff --git a/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderDAOTest.kt b/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderDAOTest.kt index d533ccc6dec..eeeef07a24e 100644 --- a/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderDAOTest.kt +++ b/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderDAOTest.kt @@ -26,6 +26,7 @@ import com.wire.kalium.persistence.db.UserDatabaseBuilder import com.wire.kalium.persistence.utils.stubs.newConversationEntity import com.wire.kalium.persistence.utils.stubs.newUserEntity import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.test.runTest import kotlin.test.BeforeTest import kotlin.test.Test @@ -209,6 +210,81 @@ class ConversationFolderDAOTest : BaseDatabaseTest() { assertEquals(conversationEntity1.id, favoriteFolderResult.first().conversationViewEntity.id) } + @Test + fun givenExistingFolder_whenRemovingFolder_thenFolderShouldBeDeleted() = runTest { + val folderId = "folderId1" + + val folder = folderWithConversationsEntity( + id = folderId, + name = "Test Folder", + type = ConversationFolderTypeEntity.USER, + conversationIdList = listOf(conversationEntity1.id) + ) + + db.conversationFolderDAO.updateConversationFolders(listOf(folder)) + assertEquals(1, db.conversationFolderDAO.getFoldersWithConversations().size) + + db.conversationFolderDAO.removeFolder(folderId) + + val result = db.conversationFolderDAO.getFoldersWithConversations() + assertTrue(result.none { it.id == folderId }) + } + + @Test + fun givenNonExistentFolder_whenRemovingFolder_thenNoErrorShouldBeThrown() = runTest { + val nonExistentFolderId = "nonExistentFolderId" + + db.conversationFolderDAO.removeFolder(nonExistentFolderId) + + val result = db.conversationFolderDAO.getFoldersWithConversations() + assertTrue(result.isEmpty()) + } + + @Test + fun givenFolderWithConversations_whenRemovingFolder_thenFolderAndConversationsShouldBeDeleted() = runTest { + val folderId = "folderId1" + + val folder = folderWithConversationsEntity( + id = folderId, + name = "Test Folder", + type = ConversationFolderTypeEntity.USER, + conversationIdList = listOf(conversationEntity1.id) + ) + + db.conversationFolderDAO.updateConversationFolders(listOf(folder)) + db.conversationFolderDAO.removeFolder(folderId) + + val folderResult = db.conversationFolderDAO.getFoldersWithConversations() + assertTrue(folderResult.none { it.id == folderId }) + + val conversationResult = db.conversationFolderDAO.observeConversationListFromFolder(folderId).firstOrNull() + assertTrue(conversationResult.isNullOrEmpty()) + } + + @Test + fun givenMultipleFolders_whenRemovingOneFolder_thenOthersShouldRemain() = runTest { + val folder1 = folderWithConversationsEntity( + id = "folderId1", + name = "Folder 1", + type = ConversationFolderTypeEntity.USER, + conversationIdList = listOf(conversationEntity1.id) + ) + + val folder2 = folderWithConversationsEntity( + id = "folderId2", + name = "Folder 2", + type = ConversationFolderTypeEntity.USER, + conversationIdList = listOf() + ) + + db.conversationFolderDAO.updateConversationFolders(listOf(folder1, folder2)) + db.conversationFolderDAO.removeFolder("folderId1") + + val result = db.conversationFolderDAO.getFoldersWithConversations() + assertEquals(1, result.size) + assertTrue(result.any { it.id == "folderId2" }) + } + companion object { fun folderWithConversationsEntity( id: String = "folderId",