Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: search inside conversation (WPB-4915) #2137

Merged
merged 16 commits into from
Oct 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,11 @@ interface MessageRepository {
targetConversation: ConversationId
): Either<StorageFailure, Unit>

suspend fun getConversationMessagesFromSearch(
searchQuery: String,
conversationId: ConversationId
): Either<CoreFailure, List<Message.Standalone>>

val extensions: MessageRepositoryExtensions
}

Expand Down Expand Up @@ -636,4 +641,14 @@ class MessageDataSource(
to = targetConversation.toDao()
)
}

override suspend fun getConversationMessagesFromSearch(
searchQuery: String,
conversationId: ConversationId
): Either<CoreFailure, List<Message.Standalone>> = wrapStorageRequest {
messageDAO.getConversationMessagesFromSearch(
searchQuery = searchQuery,
conversationId = conversationId.toDao()
).map(messageMapper::fromEntityToMessage)
}
}
Original file line number Diff line number Diff line change
@@ -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<CoreFailure, List<Message.Standalone>>] indicating the success of the operation.
*/
interface GetConversationMessagesFromSearchQueryUseCase {

suspend operator fun invoke(
searchQuery: String,
conversationId: ConversationId
): Either<CoreFailure, List<Message.Standalone>>
}

internal class GetConversationMessagesFromSearchQueryUseCaseImpl internal constructor(
private val messageRepository: MessageRepository
) : GetConversationMessagesFromSearchQueryUseCase {

override suspend fun invoke(
searchQuery: String,
conversationId: ConversationId
): Either<CoreFailure, List<Message.Standalone>> = messageRepository.getConversationMessagesFromSearch(
searchQuery = searchQuery,
conversationId = conversationId
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -321,4 +321,9 @@ class MessageScope internal constructor(
selfUserId = selfUserId,
selfConversationIdProvider = selfConversationIdProvider
)

val getConversationMessagesFromSearchQuery: GetConversationMessagesFromSearchQueryUseCase
get() = GetConversationMessagesFromSearchQueryUseCaseImpl(
messageRepository = messageRepository
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -605,6 +664,17 @@ class MessageRepositoryTest {
.thenThrow(throwable)
}

fun withMessagesFromSearch(
searchTerm: String,
conversationId: QualifiedIDEntity,
messages: List<MessageEntity>
) = apply {
given(messageDAO)
.suspendFunction(messageDAO::getConversationMessagesFromSearch)
.whenInvokedWith(eq(searchTerm), eq(conversationId))
.thenReturn(messages)
}

fun arrange() = this to MessageDataSource(
messageApi = messageApi,
mlsMessageApi = mlsMessageApi,
Expand Down Expand Up @@ -668,4 +738,3 @@ class MessageRepositoryTest {
)
}
}

Original file line number Diff line number Diff line change
@@ -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<Either.Left<StorageFailure.DataNotFound>>(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<Either.Right<List<Message.Standalone>>>(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<CoreFailure, List<Message.Standalone>>
) = 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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
alexandreferris marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -135,5 +135,10 @@ interface MessageDAO {

suspend fun moveMessages(from: ConversationIDEntity, to: ConversationIDEntity)

suspend fun getConversationMessagesFromSearch(
searchQuery: String,
conversationId: QualifiedIDEntity
): List<MessageEntity>

val platformExtensions: MessageExtensions
}
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,17 @@ internal class MessageDAOImpl internal constructor(
.distinctUntilChanged()
}

override suspend fun getConversationMessagesFromSearch(
searchQuery: String,
conversationId: QualifiedIDEntity
): List<MessageEntity> = withContext(coroutineContext) {
queries.selectConversationMessagesFromSearch(
searchQuery,
conversationId,
mapper::toEntityMessageFromView
).executeAsList()
}
alexandreferris marked this conversation as resolved.
Show resolved Hide resolved

override val platformExtensions: MessageExtensions = MessageExtensionsImpl(queries, mapper, coroutineContext)

}
Loading
Loading