From 25940016b2f27eb07d9f49270d94a79d91b72e0c Mon Sep 17 00:00:00 2001 From: Boris Safonov Date: Tue, 17 Dec 2024 16:49:07 +0200 Subject: [PATCH 1/5] feat/add_get_sender_name_by_message_id_use_case --- .../logic/data/message/MessageRepository.kt | 5 + .../GetSenderNameByMessageIdUseCase.kt | 53 +++++++++ .../logic/feature/message/MessageScope.kt | 3 + .../GetSenderNameByMessageIdUseCaseTest.kt | 108 ++++++++++++++++++ .../com/wire/kalium/persistence/Users.sq | 4 + .../persistence/dao/message/MessageDAO.kt | 1 + .../persistence/dao/message/MessageDAOImpl.kt | 6 + .../persistence/db/UserDatabaseBuilder.kt | 1 + 8 files changed, 181 insertions(+) create mode 100644 logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/GetSenderNameByMessageIdUseCase.kt create mode 100644 logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/GetSenderNameByMessageIdUseCaseTest.kt diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/MessageRepository.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/MessageRepository.kt index dd0f47b5996..272b6c85344 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/MessageRepository.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/MessageRepository.kt @@ -253,6 +253,8 @@ internal interface MessageRepository { messageId: String, conversationId: ConversationId ): Either + + suspend fun getSenderNameByMessageId(conversationId: ConversationId, messageId: String): Either } // TODO: suppress TooManyFunctions for now, something we need to fix in the future @@ -706,4 +708,7 @@ internal class MessageDataSource internal constructor( ): Either = wrapStorageRequest { messageDAO.getMessageAssetTransferStatus(messageId, conversationId.toDao()).toModel() } + + override suspend fun getSenderNameByMessageId(conversationId: ConversationId, messageId: String): Either = + wrapStorageRequest { messageDAO.getSenderNameById(messageId, conversationId.toDao()) } } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/GetSenderNameByMessageIdUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/GetSenderNameByMessageIdUseCase.kt new file mode 100644 index 00000000000..33b57989039 --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/GetSenderNameByMessageIdUseCase.kt @@ -0,0 +1,53 @@ +/* + * 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.message + +import com.wire.kalium.logic.CoreFailure +import com.wire.kalium.logic.StorageFailure +import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.data.message.MessageRepository +import com.wire.kalium.logic.functional.fold +import com.wire.kalium.util.KaliumDispatcher +import com.wire.kalium.util.KaliumDispatcherImpl +import kotlinx.coroutines.withContext + +class GetSenderNameByMessageIdUseCase internal constructor( + private val messageRepository: MessageRepository, + private val dispatchers: KaliumDispatcher = KaliumDispatcherImpl +) { + suspend operator fun invoke( + conversationId: ConversationId, + messageId: String + ): Result = withContext(dispatchers.io) { + messageRepository.getSenderNameByMessageId(conversationId, messageId).fold({ + Result.Failure(it) + }, { + Result.Success(it) + }) + } + + sealed interface Result { + + data class Success(val name: String) : Result + + /** + * [StorageFailure.DataNotFound] or some other generic error. + */ + data class Failure(val cause: CoreFailure) : Result + } +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/MessageScope.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/MessageScope.kt index bb49c7fda27..29034c28564 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/MessageScope.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/MessageScope.kt @@ -453,4 +453,7 @@ class MessageScope internal constructor( val removeMessageDraftUseCase: RemoveMessageDraftUseCase get() = RemoveMessageDraftUseCaseImpl(messageDraftRepository) + + val getSenderNameByMessageId: GetSenderNameByMessageIdUseCase + get() = GetSenderNameByMessageIdUseCase(messageRepository) } diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/GetSenderNameByMessageIdUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/GetSenderNameByMessageIdUseCaseTest.kt new file mode 100644 index 00000000000..37999004c0f --- /dev/null +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/GetSenderNameByMessageIdUseCaseTest.kt @@ -0,0 +1,108 @@ +/* + * 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.message + +import com.wire.kalium.logic.StorageFailure +import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.data.message.MessageRepository +import com.wire.kalium.logic.framework.TestConversation +import com.wire.kalium.logic.framework.TestMessage +import com.wire.kalium.logic.functional.Either +import com.wire.kalium.logic.test_util.TestKaliumDispatcher +import com.wire.kalium.util.KaliumDispatcher +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.assertEquals +import kotlin.test.assertIs + +class GetSenderNameByMessageIdUseCaseTest { + + private val testDispatchers: KaliumDispatcher = TestKaliumDispatcher + + @Test + fun givenMessageAndConversationId_whenInvokingUseCase_thenShouldCallMessageRepository() = runTest(testDispatchers.io) { + val (arrangement, getSenderNameByMessageId) = Arrangement() + .withRepositorySenderNameByMessageIdReturning(CONVERSATION_ID, MESSAGE_ID, Either.Left(StorageFailure.DataNotFound)) + .arrange() + + getSenderNameByMessageId(CONVERSATION_ID, MESSAGE_ID) + + coVerify { + arrangement.messageRepository.getMessageById(CONVERSATION_ID, MESSAGE_ID) + }.wasInvoked(exactly = once) + } + + @Test + fun givenRepositoryFails_whenInvokingUseCase_thenShouldPropagateTheFailure() = runTest(testDispatchers.io) { + val cause = StorageFailure.DataNotFound + val (_, getSenderNameByMessageId) = Arrangement() + .withRepositorySenderNameByMessageIdReturning(CONVERSATION_ID, MESSAGE_ID, Either.Left(cause)) + .arrange() + + val result = getSenderNameByMessageId(CONVERSATION_ID, MESSAGE_ID) + + assertIs(result) + assertEquals(cause, result.cause) + } + + @Test + fun givenRepositorySucceeds_whenInvokingUseCase_thenShouldPropagateTheSuccess() = runTest(testDispatchers.io) { + val (_, getSenderNameByMessageId) = Arrangement() + .withRepositorySenderNameByMessageIdReturning(CONVERSATION_ID, MESSAGE_ID, Either.Right(NAME)) + .arrange() + + val result = getSenderNameByMessageId(CONVERSATION_ID, MESSAGE_ID) + + assertIs(result) + assertEquals(NAME, result.name) + } + + private inner class Arrangement { + + @Mock + val messageRepository: MessageRepository = mock(MessageRepository::class) + + private val getSenderNameByMessageId by lazy { + GetSenderNameByMessageIdUseCase(messageRepository, testDispatchers) + } + + suspend fun withRepositorySenderNameByMessageIdReturning( + conversationId: ConversationId, + messageId: String, + response: Either + ) = apply { + coEvery { + messageRepository.getSenderNameByMessageId(conversationId, messageId) + }.returns(response) + } + + fun arrange() = this to getSenderNameByMessageId + } + + private companion object { + const val MESSAGE_ID = TestMessage.TEST_MESSAGE_ID + const val NAME = "Test User" + val CONVERSATION_ID = TestConversation.ID + } +} diff --git a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Users.sq b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Users.sq index 385d13f2602..06b200dc684 100644 --- a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Users.sq +++ b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Users.sq @@ -276,3 +276,7 @@ SELECT name, handle FROM User WHERE qualified_id = :userId; updateTeamId: UPDATE User SET team = ? WHERE qualified_id = ?; + +selectNameByMessageId: +SELECT name FROM User +WHERE qualified_id = (SELECT Message.sender_user_id FROM Message WHERE Message.id = :messageId AND Message.conversation_id = :conversationId LIMIT 1); diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageDAO.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageDAO.kt index e37edbceea0..2d78d8d85e2 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageDAO.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageDAO.kt @@ -161,4 +161,5 @@ interface MessageDAO { suspend fun observeAssetStatuses(conversationId: QualifiedIDEntity): Flow> suspend fun getMessageAssetTransferStatus(messageId: String, conversationId: QualifiedIDEntity): AssetTransferStatusEntity + suspend fun getSenderNameById(id: String, conversationId: QualifiedIDEntity): String? } diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageDAOImpl.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageDAOImpl.kt index 06d1f66ba3d..1834fa2b142 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageDAOImpl.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageDAOImpl.kt @@ -27,6 +27,7 @@ import com.wire.kalium.persistence.MessagesQueries import com.wire.kalium.persistence.NotificationQueries import com.wire.kalium.persistence.ReactionsQueries import com.wire.kalium.persistence.UnreadEventsQueries +import com.wire.kalium.persistence.UsersQueries import com.wire.kalium.persistence.content.ButtonContentQueries import com.wire.kalium.persistence.dao.ConversationIDEntity import com.wire.kalium.persistence.dao.QualifiedIDEntity @@ -59,6 +60,7 @@ internal class MessageDAOImpl internal constructor( private val messagePreviewQueries: MessagePreviewQueries, private val selfUserId: UserIDEntity, private val reactionsQueries: ReactionsQueries, + private val userQueries: UsersQueries, private val coroutineContext: CoroutineContext, private val assetStatusQueries: MessageAssetTransferStatusQueries, buttonContentQueries: ButtonContentQueries @@ -505,6 +507,10 @@ internal class MessageDAOImpl internal constructor( .executeAsOne() } + override suspend fun getSenderNameById(id: String, conversationId: QualifiedIDEntity): String? = withContext(coroutineContext) { + userQueries.selectNameByMessageId(id, conversationId).executeAsOneOrNull()?.name + } + override val platformExtensions: MessageExtensions = MessageExtensionsImpl(queries, assetViewQueries, mapper, coroutineContext) } 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 fdf7c7bac9e..76f9ab33e17 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 @@ -264,6 +264,7 @@ class UserDatabaseBuilder internal constructor( database.messagePreviewQueries, userId, database.reactionsQueries, + database.usersQueries, queriesContext, database.messageAssetTransferStatusQueries, database.buttonContentQueries From b4b66eb70a59f7f1c8178d75498d14a2cc4a0db8 Mon Sep 17 00:00:00 2001 From: Boris Safonov Date: Tue, 17 Dec 2024 21:45:48 +0200 Subject: [PATCH 2/5] Added unit test for new DB query --- .../GetSenderNameByMessageIdUseCase.kt | 4 + .../com/wire/kalium/persistence/Users.sq | 2 +- .../persistence/dao/message/MessageDAOTest.kt | 90 +++++++++++++++++++ 3 files changed, 95 insertions(+), 1 deletion(-) diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/GetSenderNameByMessageIdUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/GetSenderNameByMessageIdUseCase.kt index 33b57989039..276798ea6d3 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/GetSenderNameByMessageIdUseCase.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/GetSenderNameByMessageIdUseCase.kt @@ -26,6 +26,10 @@ import com.wire.kalium.util.KaliumDispatcher import com.wire.kalium.util.KaliumDispatcherImpl import kotlinx.coroutines.withContext +/** + * Provides a way to get a name of user that sent a message + * using its [ConversationId] and message ID coordinates. + */ class GetSenderNameByMessageIdUseCase internal constructor( private val messageRepository: MessageRepository, private val dispatchers: KaliumDispatcher = KaliumDispatcherImpl diff --git a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Users.sq b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Users.sq index 06b200dc684..1a36f2801fa 100644 --- a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Users.sq +++ b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Users.sq @@ -279,4 +279,4 @@ UPDATE User SET team = ? WHERE qualified_id = ?; selectNameByMessageId: SELECT name FROM User -WHERE qualified_id = (SELECT Message.sender_user_id FROM Message WHERE Message.id = :messageId AND Message.conversation_id = :conversationId LIMIT 1); +WHERE qualified_id = (SELECT Message.sender_user_id FROM Message WHERE Message.id = :messageId AND Message.conversation_id = :conversationId); 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 da4724ba739..4c346bd2fe0 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 @@ -2344,6 +2344,96 @@ class MessageDAOTest : BaseDatabaseTest() { assertEquals(messages.size, assetStatuses.size) } + @Test + fun givenMessagesAndUsersAreInserted_whenGettingSenderNameByMessageId_thenOnlyRelevantNameReturned() = runTest { + insertInitialData() + + val userInQuestion = userDetailsEntity1 + val otherUser = userDetailsEntity2 + + val expectedMessages = listOf( + newRegularMessageEntity( + "1", + conversationId = conversationEntity1.id, + senderUserId = userInQuestion.id, + status = MessageEntity.Status.PENDING, + senderName = userInQuestion.name!!, + sender = userInQuestion + ), + newRegularMessageEntity( + "2", + conversationId = conversationEntity1.id, + senderUserId = otherUser.id, + status = MessageEntity.Status.PENDING, + senderName = otherUser.name!!, + sender = otherUser + ) + ) + messageDAO.insertOrIgnoreMessages(expectedMessages) + + val result = messageDAO.getSenderNameById("1", conversationEntity1.id) + + assertEquals(userDetailsEntity1.name, result) + } + + @Test + fun givenMessagesAreInserted_whenGettingSenderNameByMessageId_thenOnlyRelevantNameReturned() = runTest { + insertInitialData() + + val expectedMessages = listOf( + newRegularMessageEntity( + "1", + conversationId = conversationEntity1.id, + senderUserId = userDetailsEntity1.id, + status = MessageEntity.Status.PENDING, + senderName = userDetailsEntity1.name!!, + sender = userDetailsEntity1 + ), + newRegularMessageEntity( + "2", + conversationId = conversationEntity1.id, + senderUserId = userDetailsEntity2.id, + status = MessageEntity.Status.PENDING, + senderName = userDetailsEntity2.name!!, + sender = userDetailsEntity2 + ) + ) + messageDAO.insertOrIgnoreMessages(expectedMessages) + + val result = messageDAO.getSenderNameById("1", conversationEntity1.id) + + assertEquals(userDetailsEntity1.name, result) + } + + @Test + fun givenMessagesAreButNoUserInserted_whenGettingSenderNameByMessageId_thenNullNameReturned() = runTest { + insertInitialData() + + val expectedMessages = listOf( + newRegularMessageEntity( + "1", + conversationId = conversationEntity1.id, + senderUserId = userDetailsEntity1.id.copy(value = "absolutely_another_value"), + status = MessageEntity.Status.PENDING, + senderName = "s", + sender = userDetailsEntity1.copy(name = "s", id = userDetailsEntity1.id.copy(value = "absolutely_another_value")) + ), + newRegularMessageEntity( + "2", + conversationId = conversationEntity1.id, + senderUserId = userDetailsEntity2.id, + status = MessageEntity.Status.PENDING, + senderName = userDetailsEntity2.name!!, + sender = userDetailsEntity2 + ) + ) + messageDAO.insertOrIgnoreMessages(expectedMessages) + + val result = messageDAO.getSenderNameById("1", conversationEntity1.id) + + assertEquals(null, result) + } + private suspend fun insertInitialData() { userDAO.upsertUsers(listOf(userEntity1, userEntity2)) conversationDAO.insertConversation( From c0aef5c08d32e296f36e89f196dc3906862126b3 Mon Sep 17 00:00:00 2001 From: Boris Safonov Date: Tue, 17 Dec 2024 22:26:16 +0200 Subject: [PATCH 3/5] Fixed test --- .../feature/message/GetSenderNameByMessageIdUseCaseTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/GetSenderNameByMessageIdUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/GetSenderNameByMessageIdUseCaseTest.kt index 37999004c0f..137dd1839f9 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/GetSenderNameByMessageIdUseCaseTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/GetSenderNameByMessageIdUseCaseTest.kt @@ -49,7 +49,7 @@ class GetSenderNameByMessageIdUseCaseTest { getSenderNameByMessageId(CONVERSATION_ID, MESSAGE_ID) coVerify { - arrangement.messageRepository.getMessageById(CONVERSATION_ID, MESSAGE_ID) + arrangement.messageRepository.getSenderNameByMessageId(CONVERSATION_ID, MESSAGE_ID) }.wasInvoked(exactly = once) } From dea30b6d57aec0072cf20c31fc2bd5dc233b3f60 Mon Sep 17 00:00:00 2001 From: Boris Safonov Date: Fri, 20 Dec 2024 17:15:58 +0200 Subject: [PATCH 4/5] feat: GetNextAudioMessageInConversationUseCase --- .../logic/data/message/MessageRepository.kt | 7 ++ ...etNextAudioMessageInConversationUseCase.kt | 53 +++++++++ .../logic/feature/message/MessageScope.kt | 3 + ...xtAudioMessageInConversationUseCaseTest.kt | 109 ++++++++++++++++++ .../com/wire/kalium/persistence/Messages.sq | 8 ++ .../persistence/dao/message/MessageDAO.kt | 1 + .../persistence/dao/message/MessageDAOImpl.kt | 5 + .../persistence/dao/message/MessageDAOTest.kt | 82 ++++++++++++- 8 files changed, 262 insertions(+), 6 deletions(-) create mode 100644 logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/GetNextAudioMessageInConversationUseCase.kt create mode 100644 logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/GetNextAudioMessageInConversationUseCaseTest.kt diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/MessageRepository.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/MessageRepository.kt index 272b6c85344..0e1902baba4 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/MessageRepository.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/MessageRepository.kt @@ -255,6 +255,7 @@ internal interface MessageRepository { ): Either suspend fun getSenderNameByMessageId(conversationId: ConversationId, messageId: String): Either + suspend fun getNextAudioMessageInConversation(conversationId: ConversationId, messageId: String): Either } // TODO: suppress TooManyFunctions for now, something we need to fix in the future @@ -711,4 +712,10 @@ internal class MessageDataSource internal constructor( override suspend fun getSenderNameByMessageId(conversationId: ConversationId, messageId: String): Either = wrapStorageRequest { messageDAO.getSenderNameById(messageId, conversationId.toDao()) } + + override suspend fun getNextAudioMessageInConversation( + conversationId: ConversationId, + messageId: String + ): Either = + wrapStorageRequest { messageDAO.getNextAudioMessageInConversation(messageId, conversationId.toDao()) } } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/GetNextAudioMessageInConversationUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/GetNextAudioMessageInConversationUseCase.kt new file mode 100644 index 00000000000..78071f16970 --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/GetNextAudioMessageInConversationUseCase.kt @@ -0,0 +1,53 @@ +/* + * 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.message + +import com.wire.kalium.logic.CoreFailure +import com.wire.kalium.logic.StorageFailure +import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.data.message.MessageRepository +import com.wire.kalium.logic.functional.fold +import com.wire.kalium.util.KaliumDispatcher +import com.wire.kalium.util.KaliumDispatcherImpl +import kotlinx.coroutines.withContext + +/** + * Provides a way to get a messageId of next AudioMessage after [messageId] in [ConversationId] conversation. + */ +class GetNextAudioMessageInConversationUseCase internal constructor( + private val messageRepository: MessageRepository, + private val dispatchers: KaliumDispatcher = KaliumDispatcherImpl +) { + suspend operator fun invoke( + conversationId: ConversationId, + messageId: String + ): Result = withContext(dispatchers.io) { + messageRepository.getNextAudioMessageInConversation(conversationId, messageId) + .fold({ Result.Failure(it) }, { Result.Success(it) }) + } + + sealed interface Result { + + data class Success(val messageId: String) : Result + + /** + * [StorageFailure.DataNotFound] in case there is no AudioMessage or some other generic error. + */ + data class Failure(val cause: CoreFailure) : Result + } +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/MessageScope.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/MessageScope.kt index 29034c28564..4a9a1b36d07 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/MessageScope.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/MessageScope.kt @@ -456,4 +456,7 @@ class MessageScope internal constructor( val getSenderNameByMessageId: GetSenderNameByMessageIdUseCase get() = GetSenderNameByMessageIdUseCase(messageRepository) + + val getNextAudioMessageInConversation: GetNextAudioMessageInConversationUseCase + get() = GetNextAudioMessageInConversationUseCase(messageRepository) } diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/GetNextAudioMessageInConversationUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/GetNextAudioMessageInConversationUseCaseTest.kt new file mode 100644 index 00000000000..a869cd8938a --- /dev/null +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/GetNextAudioMessageInConversationUseCaseTest.kt @@ -0,0 +1,109 @@ +/* + * 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.message + +import com.wire.kalium.logic.StorageFailure +import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.data.message.Message +import com.wire.kalium.logic.data.message.MessageRepository +import com.wire.kalium.logic.framework.TestConversation +import com.wire.kalium.logic.framework.TestMessage +import com.wire.kalium.logic.functional.Either +import com.wire.kalium.logic.test_util.TestKaliumDispatcher +import com.wire.kalium.util.KaliumDispatcher +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.assertEquals +import kotlin.test.assertIs + +class GetNextAudioMessageInConversationUseCaseTest { + + private val testDispatchers: KaliumDispatcher = TestKaliumDispatcher + + @Test + fun givenMessageAndConversationId_whenInvokingUseCase_thenShouldCallMessageRepository() = runTest(testDispatchers.io) { + val (arrangement, getMessageByIdUseCase) = Arrangement() + .withRepositoryMessageByIdReturning(CONVERSATION_ID, MESSAGE_ID, Either.Left(StorageFailure.DataNotFound)) + .arrange() + + getMessageByIdUseCase(CONVERSATION_ID, MESSAGE_ID) + + coVerify { + arrangement.messageRepository.getMessageById(CONVERSATION_ID, MESSAGE_ID) + }.wasInvoked(exactly = once) + } + + @Test + fun givenRepositoryFails_whenInvokingUseCase_thenShouldPropagateTheFailure() = runTest(testDispatchers.io) { + val cause = StorageFailure.DataNotFound + val (_, getMessageByIdUseCase) = Arrangement() + .withRepositoryMessageByIdReturning(CONVERSATION_ID, MESSAGE_ID, Either.Left(cause)) + .arrange() + + val result = getMessageByIdUseCase(CONVERSATION_ID, MESSAGE_ID) + + assertIs(result) + assertEquals(cause, result.cause) + } + + @Test + fun givenRepositorySucceeds_whenInvokingUseCase_thenShouldPropagateTheSuccess() = runTest(testDispatchers.io) { + val (_, getMessageByIdUseCase) = Arrangement() + .withRepositoryMessageByIdReturning(CONVERSATION_ID, MESSAGE_ID, Either.Right(MESSAGE)) + .arrange() + + val result = getMessageByIdUseCase(CONVERSATION_ID, MESSAGE_ID) + + assertIs(result) + assertEquals(MESSAGE, result.message) + } + + private inner class Arrangement { + + @Mock + val messageRepository: MessageRepository = mock(MessageRepository::class) + + private val getMessageById by lazy { + GetMessageByIdUseCase(messageRepository, testDispatchers) + } + + suspend fun withRepositoryMessageByIdReturning( + conversationId: ConversationId, + messageId: String, + response: Either + ) = apply { + coEvery { + messageRepository.getMessageById(conversationId, messageId) + }.returns(response) + } + + fun arrange() = this to getMessageById + } + + private companion object { + const val MESSAGE_ID = TestMessage.TEST_MESSAGE_ID + val MESSAGE = TestMessage.TEXT_MESSAGE + val CONVERSATION_ID = TestConversation.ID + } +} 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 90308374887..a25d9a938bb 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 @@ -608,3 +608,11 @@ WHERE conversation_id = :conversationId AND creation_date >= (SELECT creation_date FROM Message WHERE id = :messageId LIMIT 1) AND expire_after_millis IS NULL ORDER BY creation_date DESC; + +selectNextAudioMessage: +SELECT Message.id +FROM Message LEFT JOIN MessageAssetContent AS AssetContent ON Message.id = AssetContent.message_id AND Message.conversation_id = AssetContent.conversation_id +WHERE Message.conversation_id = :conversationId +AND AssetContent.asset_mime_type LIKE "%audio/%" +AND Message.creation_date > (SELECT Message.creation_date FROM Message WHERE Message.id = :messageId AND Message.conversation_id = :conversationId) +LIMIT 1; diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageDAO.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageDAO.kt index 2d78d8d85e2..25884bfffc3 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageDAO.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageDAO.kt @@ -162,4 +162,5 @@ interface MessageDAO { suspend fun observeAssetStatuses(conversationId: QualifiedIDEntity): Flow> suspend fun getMessageAssetTransferStatus(messageId: String, conversationId: QualifiedIDEntity): AssetTransferStatusEntity suspend fun getSenderNameById(id: String, conversationId: QualifiedIDEntity): String? + suspend fun getNextAudioMessageInConversation(prevMessageId: String, conversationId: QualifiedIDEntity): String? } diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageDAOImpl.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageDAOImpl.kt index 1834fa2b142..40f89f6068f 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageDAOImpl.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageDAOImpl.kt @@ -511,6 +511,11 @@ internal class MessageDAOImpl internal constructor( userQueries.selectNameByMessageId(id, conversationId).executeAsOneOrNull()?.name } + override suspend fun getNextAudioMessageInConversation(prevMessageId: String, conversationId: QualifiedIDEntity): String? = + withContext(coroutineContext) { + queries.selectNextAudioMessage(conversationId, prevMessageId).executeAsOneOrNull() + } + override val platformExtensions: MessageExtensions = MessageExtensionsImpl(queries, assetViewQueries, mapper, coroutineContext) } 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 4c346bd2fe0..a365283d308 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 @@ -2351,7 +2351,7 @@ class MessageDAOTest : BaseDatabaseTest() { val userInQuestion = userDetailsEntity1 val otherUser = userDetailsEntity2 - val expectedMessages = listOf( + val insertingMessages = listOf( newRegularMessageEntity( "1", conversationId = conversationEntity1.id, @@ -2369,7 +2369,7 @@ class MessageDAOTest : BaseDatabaseTest() { sender = otherUser ) ) - messageDAO.insertOrIgnoreMessages(expectedMessages) + messageDAO.insertOrIgnoreMessages(insertingMessages) val result = messageDAO.getSenderNameById("1", conversationEntity1.id) @@ -2380,7 +2380,7 @@ class MessageDAOTest : BaseDatabaseTest() { fun givenMessagesAreInserted_whenGettingSenderNameByMessageId_thenOnlyRelevantNameReturned() = runTest { insertInitialData() - val expectedMessages = listOf( + val insertingMessages = listOf( newRegularMessageEntity( "1", conversationId = conversationEntity1.id, @@ -2398,7 +2398,7 @@ class MessageDAOTest : BaseDatabaseTest() { sender = userDetailsEntity2 ) ) - messageDAO.insertOrIgnoreMessages(expectedMessages) + messageDAO.insertOrIgnoreMessages(insertingMessages) val result = messageDAO.getSenderNameById("1", conversationEntity1.id) @@ -2409,7 +2409,7 @@ class MessageDAOTest : BaseDatabaseTest() { fun givenMessagesAreButNoUserInserted_whenGettingSenderNameByMessageId_thenNullNameReturned() = runTest { insertInitialData() - val expectedMessages = listOf( + val insertingMessages = listOf( newRegularMessageEntity( "1", conversationId = conversationEntity1.id, @@ -2427,13 +2427,33 @@ class MessageDAOTest : BaseDatabaseTest() { sender = userDetailsEntity2 ) ) - messageDAO.insertOrIgnoreMessages(expectedMessages) + messageDAO.insertOrIgnoreMessages(insertingMessages) val result = messageDAO.getSenderNameById("1", conversationEntity1.id) assertEquals(null, result) } + @Test + fun givenAudioMessagesAreInserted_whenGettingNextAudioMessageAfterTheLastOne_thenNullIdReturned() = runTest { + insertInitialData() + messageDAO.insertOrIgnoreMessages(listOfMessageWithAudioAssets()) + + val result = messageDAO.getNextAudioMessageInConversation("4", conversationEntity1.id) + + assertEquals(null, result) + } + + @Test + fun givenAudioMessagesAreInserted_whenGettingNextAudioMessageAfterTheFirstOne_thenCorrespondingIdReturned() = runTest { + insertInitialData() + messageDAO.insertOrIgnoreMessages(listOfMessageWithAudioAssets()) + + val result = messageDAO.getNextAudioMessageInConversation("1", conversationEntity1.id) + + assertEquals("3", result) + } + private suspend fun insertInitialData() { userDAO.upsertUsers(listOf(userEntity1, userEntity2)) conversationDAO.insertConversation( @@ -2479,4 +2499,54 @@ class MessageDAOTest : BaseDatabaseTest() { ), visibility = if (isComplete) MessageEntity.Visibility.VISIBLE else MessageEntity.Visibility.HIDDEN ) + + private fun listOfMessageWithAudioAssets(): List { + val messageTemplate = newRegularMessageEntity( + conversationId = conversationEntity1.id, + senderUserId = userDetailsEntity1.id, + status = MessageEntity.Status.DELIVERED, + sender = userDetailsEntity1, + content = MessageEntityContent.Asset( + assetSizeInBytes = 1000, + assetMimeType = "audio/mp4", + assetOtrKey = byteArrayOf(1), + assetSha256Key = byteArrayOf(1), + assetId = "assetId", + assetEncryptionAlgorithm = "", + assetDurationMs = 10 + ) + ) + + return listOf( + messageTemplate.copy(id = "1", date = messageTemplate.date.plus(10.seconds)), + messageTemplate.copy( + id = "2", + date = messageTemplate.date.plus(20.seconds), + content = MessageEntityContent.Text("Test Text") + ), + messageTemplate.copy(id = "3", date = messageTemplate.date.plus(30.seconds)), + messageTemplate.copy(id = "4", date = messageTemplate.date.plus(40.seconds)), + newRegularMessageEntity( + id = "5", + conversationId = conversationEntity1.id, + senderUserId = userDetailsEntity1.id, + status = MessageEntity.Status.DELIVERED, + sender = userDetailsEntity1, + date = messageTemplate.date.plus(50.seconds) + ), + messageTemplate.copy( + id = "6", + date = messageTemplate.date.plus(60.seconds), + content = MessageEntityContent.Asset( + assetSizeInBytes = 1000, + assetMimeType = "video/mp4", + assetOtrKey = byteArrayOf(1), + assetSha256Key = byteArrayOf(1), + assetId = "assetId", + assetEncryptionAlgorithm = "", + assetDurationMs = 10 + ) + ) + ) + } } From 47045db11788651708acfec59b4c423c9636dce5 Mon Sep 17 00:00:00 2001 From: Boris Safonov Date: Mon, 23 Dec 2024 14:33:54 +0200 Subject: [PATCH 5/5] Fixed code style --- .../kotlin/com/wire/kalium/logic/feature/message/MessageScope.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/MessageScope.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/MessageScope.kt index eec69697869..4a9a1b36d07 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/MessageScope.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/MessageScope.kt @@ -62,7 +62,6 @@ import com.wire.kalium.logic.feature.asset.UpdateAssetMessageTransferStatusUseCa import com.wire.kalium.logic.feature.asset.UpdateAssetMessageTransferStatusUseCaseImpl import com.wire.kalium.logic.feature.asset.ValidateAssetFileTypeUseCase import com.wire.kalium.logic.feature.asset.ValidateAssetFileTypeUseCaseImpl -import com.wire.kalium.logic.feature.incallreaction.SendInCallReactionUseCase import com.wire.kalium.logic.feature.message.composite.SendButtonActionConfirmationMessageUseCase import com.wire.kalium.logic.feature.message.composite.SendButtonActionMessageUseCase import com.wire.kalium.logic.feature.message.composite.SendButtonMessageUseCase