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 f0252254f71..bc6f895d48c 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 @@ -219,6 +219,11 @@ interface MessageRepository { targetConversation: ConversationId ): Either + suspend fun getConversationMessagesFromSearch( + searchQuery: String, + conversationId: ConversationId + ): Either> + val extensions: MessageRepositoryExtensions } @@ -636,4 +641,14 @@ class MessageDataSource( to = targetConversation.toDao() ) } + + override suspend fun getConversationMessagesFromSearch( + searchQuery: String, + conversationId: ConversationId + ): Either> = wrapStorageRequest { + messageDAO.getConversationMessagesFromSearch( + searchQuery = searchQuery, + conversationId = conversationId.toDao() + ).map(messageMapper::fromEntityToMessage) + } } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/GetConversationMessagesFromSearchQueryUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/GetConversationMessagesFromSearchQueryUseCase.kt new file mode 100644 index 00000000000..64773932e1e --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/GetConversationMessagesFromSearchQueryUseCase.kt @@ -0,0 +1,51 @@ +/* + * Wire + * Copyright (C) 2023 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.data.id.ConversationId +import com.wire.kalium.logic.data.message.Message +import com.wire.kalium.logic.data.message.MessageRepository +import com.wire.kalium.logic.functional.Either + +/** + * Retrieves a list of messages defined by the search query. + * @param searchQuery The search term used to define which messages will be returned. + * @param conversationId The conversation ID that it will look for messages in. + * @return A [Either>] indicating the success of the operation. + */ +interface GetConversationMessagesFromSearchQueryUseCase { + + suspend operator fun invoke( + searchQuery: String, + conversationId: ConversationId + ): Either> +} + +internal class GetConversationMessagesFromSearchQueryUseCaseImpl internal constructor( + private val messageRepository: MessageRepository +) : GetConversationMessagesFromSearchQueryUseCase { + + override suspend fun invoke( + searchQuery: String, + conversationId: ConversationId + ): Either> = messageRepository.getConversationMessagesFromSearch( + searchQuery = searchQuery, + conversationId = conversationId + ) +} 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 3313769ccda..800b735e1b9 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 @@ -321,4 +321,9 @@ class MessageScope internal constructor( selfUserId = selfUserId, selfConversationIdProvider = selfConversationIdProvider ) + + val getConversationMessagesFromSearchQuery: GetConversationMessagesFromSearchQueryUseCase + get() = GetConversationMessagesFromSearchQueryUseCaseImpl( + messageRepository = messageRepository + ) } diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/message/MessageRepositoryTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/message/MessageRepositoryTest.kt index f358ae99fb7..a761309bb16 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/message/MessageRepositoryTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/message/MessageRepositoryTest.kt @@ -57,7 +57,6 @@ import io.mockative.matching import io.mockative.mock import io.mockative.once import io.mockative.verify -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOf @@ -472,6 +471,58 @@ class MessageRepositoryTest { } } + @Test + fun givenConversationWithMessages_whenSearchingForSpecificMessages_thenReturnOnlyMetCriteriaMessages() = runTest { + // given + val qualifiedIdEntity = TEST_QUALIFIED_ID_ENTITY + val conversationId = TEST_CONVERSATION_ID + val searchTerm = "message 1" + + val messageEntity1 = TEST_MESSAGE_ENTITY.copy( + id = "msg1", + conversationId = qualifiedIdEntity, + content = MessageEntityContent.Text("message 10") + ) + + val messages = listOf(messageEntity1) + + val message1 = TEST_MESSAGE.copy( + id = "msg1", + conversationId = conversationId, + content = MessageContent.Text("message 10") + ) + + val expectedMessages = listOf(message1) + + val (_, messageRepository) = Arrangement() + .withMessagesFromSearch( + searchTerm = searchTerm, + conversationId = qualifiedIdEntity, + messages = messages + ) + .withMappedMessageModel( + result = message1, + param = messageEntity1 + ) + .arrange() + + // when + val result = messageRepository.getConversationMessagesFromSearch( + searchQuery = searchTerm, + conversationId = conversationId + ) + + // then + assertEquals( + expectedMessages.size, + (result as Either.Right).value.size + ) + assertEquals( + expectedMessages.first().id, + (result as Either.Right).value.first().id + ) + } + private class Arrangement { @Mock @@ -514,6 +565,14 @@ class MessageRepositoryTest { return this } + fun withMappedMessageModel(result: Message.Regular, param: MessageEntity.Regular): Arrangement { + given(messageMapper) + .function(messageMapper::fromEntityToMessage) + .whenInvokedWith(eq(param)) + .then { result } + return this + } + fun withMappedMessageEntity(message: MessageEntity.Regular): Arrangement { given(messageMapper) .function(messageMapper::fromMessageToEntity) @@ -605,6 +664,17 @@ class MessageRepositoryTest { .thenThrow(throwable) } + fun withMessagesFromSearch( + searchTerm: String, + conversationId: QualifiedIDEntity, + messages: List + ) = apply { + given(messageDAO) + .suspendFunction(messageDAO::getConversationMessagesFromSearch) + .whenInvokedWith(eq(searchTerm), eq(conversationId)) + .thenReturn(messages) + } + fun arrange() = this to MessageDataSource( messageApi = messageApi, mlsMessageApi = mlsMessageApi, @@ -668,4 +738,3 @@ class MessageRepositoryTest { ) } } - diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/GetConversationMessagesFromSearchQueryUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/GetConversationMessagesFromSearchQueryUseCaseTest.kt new file mode 100644 index 00000000000..5f88459a649 --- /dev/null +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/GetConversationMessagesFromSearchQueryUseCaseTest.kt @@ -0,0 +1,136 @@ +/* + * Wire + * Copyright (C) 2023 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.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 io.mockative.Mock +import io.mockative.given +import io.mockative.mock +import io.mockative.once +import io.mockative.verify +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs + +class GetConversationMessagesFromSearchQueryUseCaseTest { + + @Test + fun givenSearchTermAndConversationId_whenInvokingUseCase_thenShouldCallMessageRepository() = runTest { + // given + val searchTerm = "message 1" + val (arrangement, useCase) = Arrangement() + .withRepositoryMessagesBySearchTermReturning( + conversationId = CONVERSATION_ID, + searchTerm = searchTerm, + response = Either.Left(StorageFailure.DataNotFound) + ) + .arrange() + + // when + useCase( + searchQuery = searchTerm, + conversationId = CONVERSATION_ID + ) + + // then + verify(arrangement.messageRepository) + .coroutine { arrangement.messageRepository.getConversationMessagesFromSearch(searchTerm, CONVERSATION_ID) } + .wasInvoked(exactly = once) + } + + @Test + fun givenRepositoryFails_whenInvokingUseCase_thenShouldPropagateTheFailure() = runTest { + // given + val searchTerm = "message 1" + val cause = StorageFailure.DataNotFound + val (_, useCase) = Arrangement() + .withRepositoryMessagesBySearchTermReturning( + conversationId = CONVERSATION_ID, + searchTerm = searchTerm, + response = Either.Left(cause) + ) + .arrange() + + // when + val result = useCase( + searchQuery = searchTerm, + conversationId = CONVERSATION_ID + ) + + // then + assertIs>(result) + assertEquals(cause, result.value) + } + + @Test + fun givenRepositorySucceeds_whenInvokingUseCase_thenShouldPropagateTheSuccess() = runTest { + // given + val searchTerm = "message 1" + val (_, useCase) = Arrangement() + .withRepositoryMessagesBySearchTermReturning( + conversationId = CONVERSATION_ID, + searchTerm = searchTerm, + response = Either.Right(MESSAGES) + ) + .arrange() + + // when + val result = useCase( + searchQuery = searchTerm, + conversationId = CONVERSATION_ID + ) + + // then + assertIs>>(result) + } + + private inner class Arrangement { + + @Mock + val messageRepository: MessageRepository = mock(MessageRepository::class) + + private val getMessageById by lazy { + GetConversationMessagesFromSearchQueryUseCaseImpl(messageRepository) + } + + suspend fun withRepositoryMessagesBySearchTermReturning( + conversationId: ConversationId, + searchTerm: String, + response: Either> + ) = apply { + given(messageRepository) + .coroutine { messageRepository.getConversationMessagesFromSearch(searchTerm, conversationId) } + .thenReturn(response) + } + + fun arrange() = this to getMessageById + } + + private companion object { + val MESSAGES = listOf(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 cb110bb1f79..f5ac7e4e986 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 @@ -511,3 +511,10 @@ moveMessages: UPDATE OR REPLACE Message SET conversation_id = :to WHERE conversation_id = :from; + +selectConversationMessagesFromSearch: +SELECT * FROM MessageDetailsView +WHERE text LIKE ('%' || :searchQuery || '%') +AND conversationId = :conversationId +AND contentType = 'TEXT' +ORDER BY date DESC; 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 54db47261c2..029915ea4ae 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 @@ -135,5 +135,10 @@ interface MessageDAO { suspend fun moveMessages(from: ConversationIDEntity, to: ConversationIDEntity) + suspend fun getConversationMessagesFromSearch( + searchQuery: String, + conversationId: QualifiedIDEntity + ): List + val platformExtensions: MessageExtensions } 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 60f119f0dd9..95991d674dd 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 @@ -413,6 +413,17 @@ internal class MessageDAOImpl internal constructor( .distinctUntilChanged() } + override suspend fun getConversationMessagesFromSearch( + searchQuery: String, + conversationId: QualifiedIDEntity + ): List = withContext(coroutineContext) { + queries.selectConversationMessagesFromSearch( + searchQuery, + conversationId, + mapper::toEntityMessageFromView + ).executeAsList() + } + override val platformExtensions: MessageExtensions = MessageExtensionsImpl(queries, 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 03e554265b8..5d69ad9f42d 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 @@ -18,7 +18,6 @@ package com.wire.kalium.persistence.dao.message -import app.cash.turbine.test import com.wire.kalium.persistence.BaseDatabaseTest import com.wire.kalium.persistence.dao.QualifiedIDEntity import com.wire.kalium.persistence.dao.UserDAO @@ -1676,7 +1675,8 @@ class MessageDAOTest : BaseDatabaseTest() { assertEquals( allMessages.map { it.content }.toSet(), - retrievedMessages.map { it.content }.toSet()) + retrievedMessages.map { it.content }.toSet() + ) } @Test @@ -1717,7 +1717,8 @@ class MessageDAOTest : BaseDatabaseTest() { assertEquals( allMessages.map { it.content }.toSet(), - retrievedMessages.map { it.content }.toSet()) + retrievedMessages.map { it.content }.toSet() + ) } @Test @@ -1758,7 +1759,68 @@ class MessageDAOTest : BaseDatabaseTest() { assertEquals( allMessages.map { it.content }.toSet(), - retrievedMessages.map { it.content }.toSet()) + retrievedMessages.map { it.content }.toSet() + ) + } + + @Test + fun givenMessagesAreInserted_whenGettingConversationMessagesFromSearch_thenOnlyRelevantMessagesAreReturned() = runTest { + insertInitialData() + + val userInQuestion = userEntity1 + val otherUser = userEntity2 + + val expectedMessages = listOf( + newRegularMessageEntity( + "1", + conversationId = conversationEntity1.id, + status = MessageEntity.Status.SENT, + senderUserId = otherUser.id, + senderName = otherUser.name!!, + content = MessageEntityContent.Text("message10"), + date = Instant.parse("2022-03-30T15:36:00.000Z") + ), + newRegularMessageEntity( + "2", + conversationId = conversationEntity1.id, + status = MessageEntity.Status.SENT, + senderUserId = otherUser.id, + senderName = otherUser.name!!, + content = MessageEntityContent.Text("message11"), + date = Instant.parse("2022-03-30T15:36:01.000Z") + ) + ) + + val allMessages = expectedMessages + listOf( + newRegularMessageEntity( + "3", + conversationId = conversationEntity1.id, + senderUserId = otherUser.id, + senderName = otherUser.name!!, + status = MessageEntity.Status.SENT, + content = MessageEntityContent.Text("message20"), + date = Instant.parse("2022-03-30T15:36:02.000Z") + ), + newRegularMessageEntity( + "4", + conversationId = conversationEntity1.id, + senderUserId = otherUser.id, + senderName = otherUser.name!!, + status = MessageEntity.Status.SENT, + content = MessageEntityContent.Text("message21"), + date = Instant.parse("2022-03-30T15:36:03.000Z") + ) + ) + + messageDAO.insertOrIgnoreMessages(allMessages) + + val result = messageDAO.getConversationMessagesFromSearch( + searchQuery = "message1", + conversationId = conversationEntity1.id + ) + + assertEquals(expectedMessages.size, result.size) + assertContentEquals(expectedMessages.sortedByDescending { it.date }, result) } private suspend fun insertInitialData() {