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 272b6c8534..0e1902baba 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 0000000000..78071f1697 --- /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 9b5f3dcbd4..4a9a1b36d0 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 @@ -455,15 +454,9 @@ class MessageScope internal constructor( val removeMessageDraftUseCase: RemoveMessageDraftUseCase get() = RemoveMessageDraftUseCaseImpl(messageDraftRepository) - val sendInCallReactionUseCase: SendInCallReactionUseCase - get() = SendInCallReactionUseCase( - selfUserId = selfUserId, - provideClientId = currentClientIdProvider, - messageSender = messageSender, - dispatchers = dispatcher, - scope = scope, - ) - 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 0000000000..a869cd8938 --- /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 9030837488..a25d9a938b 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 2d78d8d85e..25884bfffc 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 1834fa2b14..40f89f6068 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 4c346bd2fe..a365283d30 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 + ) + ) + ) + } }