Skip to content

Commit

Permalink
feat: count searched message position (WPB-4986) (#2186)
Browse files Browse the repository at this point in the history
* feat: add messages query to get count of messages until selected searched message

* feat: add usage of query into MessageDAO

* feat: add usage of getSearchedConversationMessagePosition into message repository

* feat: create getSearchedConversationMessagePosition use case and add to MessageScope

* chore: change return type to Int

* chore: add result type for use case

* refactor: fetch initial offset instead of expanding the limit

* chore: add use case docs

* feat: add custom KaliumOffsetQueryPagingSource

* chore: add ComplexMethod annotation on overriden OffsetQueryPagingSource

* test: add tests for MessageDAO selecting selected message position

* test: add tests for message repository extensions

* test: add tests for message extensions

* test: add tests for selected message position

* chore: adjust Result to sealed interface

* test: add tests for GetSearchedConversationMessagePositionUseCase

* chore: add docs to KaliumOffsetQueryPagingSource

* chore: rename test variables

* chore: adjust failure return of usecase

* chore: remove unused import

---------

Co-authored-by: mohamad.jaara <[email protected]>
  • Loading branch information
alexandreferris and MohamadJaara authored Nov 9, 2023
1 parent e7e24ea commit eb47fb6
Show file tree
Hide file tree
Showing 15 changed files with 478 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ class MessageRepositoryExtensionsTest {
fun givenParameters_whenPaginatedMessagesByConversation_thenShouldCallDaoExtensionsWithRightParameters() = runTest {
val pagingConfig = PagingConfig(20)
val pager = Pager(pagingConfig) { fakePagingSource }
val startingOffset = 0

val kaliumPager = KaliumPager(pager, fakePagingSource, StandardTestDispatcher())
val (arrangement, messageRepositoryExtensions) = Arrangement()
Expand All @@ -65,7 +66,8 @@ class MessageRepositoryExtensionsTest {
messageRepositoryExtensions.getPaginatedMessagesByConversationIdAndVisibility(
TestConversation.ID,
visibilities,
pagingConfig
pagingConfig,
startingOffset
)

verify(arrangement.messageDaoExtensions)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ actual interface MessageRepositoryExtensions {
conversationId: ConversationId,
visibility: List<Message.Visibility>,
pagingConfig: PagingConfig,
startingOffset: Int
): Flow<PagingData<Message.Standalone>>
}

Expand All @@ -46,11 +47,13 @@ actual class MessageRepositoryExtensionsImpl actual constructor(
conversationId: ConversationId,
visibility: List<Message.Visibility>,
pagingConfig: PagingConfig,
startingOffset: Int
): Flow<PagingData<Message.Standalone>> {
val pager: KaliumPager<MessageEntity> = messageDAO.platformExtensions.getPagerForConversation(
conversationId.toDao(),
visibility.map { it.toEntityVisibility() },
pagingConfig
pagingConfig,
startingOffset
)

return pager.pagingDataFlow.map { pagingData: PagingData<MessageEntity> ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,9 @@ class GetPaginatedFlowOfMessagesByConversationUseCase internal constructor(
suspend operator fun invoke(
conversationId: ConversationId,
visibility: List<Message.Visibility> = Message.Visibility.values().toList(),
startingOffset: Int,
pagingConfig: PagingConfig
): Flow<PagingData<Message.Standalone>> = messageRepository.extensions.getPaginatedMessagesByConversationIdAndVisibility(
conversationId, visibility, pagingConfig
conversationId, visibility, pagingConfig, startingOffset
).flowOn(dispatcher.io)
}
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,11 @@ interface MessageRepository {
conversationId: ConversationId
): Either<CoreFailure, List<Message.Standalone>>

suspend fun getSearchedConversationMessagePosition(
conversationId: ConversationId,
messageId: String
): Either<StorageFailure, Int>

val extensions: MessageRepositoryExtensions
}

Expand Down Expand Up @@ -649,4 +654,14 @@ class MessageDataSource(
conversationId = conversationId.toDao()
).map(messageMapper::fromEntityToMessage)
}

override suspend fun getSearchedConversationMessagePosition(
conversationId: ConversationId,
messageId: String
): Either<StorageFailure, Int> = wrapStorageRequest {
messageDAO.getSearchedConversationMessagePosition(
conversationId = conversationId.toDao(),
messageId = messageId
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* 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.MessageRepository
import com.wire.kalium.logic.functional.fold

/**
* Gets the Selected Message Position from Search
*
* @param conversationId [ConversationId] the id of the conversation where the search is happening.
* @param messageId [String] the id of the selected message from search.
*
* @result [Result] Success with Int position. Failure with StorageFailure.
*/
interface GetSearchedConversationMessagePositionUseCase {

suspend operator fun invoke(
conversationId: ConversationId,
messageId: String
): Result

sealed interface Result {
data class Success(val position: Int) : Result
data class Failure(val cause: CoreFailure) : Result
}
}

internal class GetSearchedConversationMessagePositionUseCaseImpl internal constructor(
private val messageRepository: MessageRepository
) : GetSearchedConversationMessagePositionUseCase {

override suspend fun invoke(
conversationId: ConversationId,
messageId: String
): GetSearchedConversationMessagePositionUseCase.Result = messageRepository
.getSearchedConversationMessagePosition(
conversationId = conversationId,
messageId = messageId
).fold(
{ GetSearchedConversationMessagePositionUseCase.Result.Failure(it) },
{ GetSearchedConversationMessagePositionUseCase.Result.Success(it) }
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -328,4 +328,9 @@ class MessageScope internal constructor(
get() = GetConversationMessagesFromSearchQueryUseCaseImpl(
messageRepository = messageRepository
)

val getSearchedConversationMessagePosition: GetSearchedConversationMessagePositionUseCase
get() = GetSearchedConversationMessagePositionUseCaseImpl(
messageRepository = messageRepository
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,38 @@ class MessageRepositoryTest {
)
}

@Test
fun givenSearchedMessages_whenMessageIsSelected_thenReturnMessagePosition() = runTest {
// given
val qualifiedIdEntity = TEST_QUALIFIED_ID_ENTITY
val conversationId = TEST_CONVERSATION_ID
val message = TEST_MESSAGE_ENTITY.copy(
id = "msg1",
conversationId = qualifiedIdEntity,
content = MessageEntityContent.Text("message 1")
)
val expectedMessagePosition = 113
val (_, messageRepository) = Arrangement()
.withSelectedMessagePosition(
conversationId = conversationId.toDao(),
messageId = message.id,
result = expectedMessagePosition
)
.arrange()

// when
val result = messageRepository.getSearchedConversationMessagePosition(
conversationId = conversationId,
messageId = message.id
)

// then
assertEquals(
expectedMessagePosition,
(result as Either.Right).value
)
}

private class Arrangement {

@Mock
Expand Down Expand Up @@ -673,6 +705,17 @@ class MessageRepositoryTest {
.thenReturn(messages)
}

fun withSelectedMessagePosition(
conversationId: QualifiedIDEntity,
messageId: String,
result: Int
) = apply {
given(messageDAO)
.suspendFunction(messageDAO::getSearchedConversationMessagePosition)
.whenInvokedWith(eq(conversationId), eq(messageId))
.thenReturn(result)
}

fun arrange() = this to MessageDataSource(
messageApi = messageApi,
mlsMessageApi = mlsMessageApi,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/*
* 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.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.feature.message.GetSearchedConversationMessagePositionUseCase
import com.wire.kalium.logic.feature.message.GetSearchedConversationMessagePositionUseCaseImpl
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 GetSearchedConversationMessagePositionUseCaseTest {

@Test
fun givenConversationIdAndMessageId_whenInvokingUseCase_thenShouldCallMessageRepository() = runTest {
val (arrangement, getSearchedConversationMessagePosition) = Arrangement()
.withRepositoryMessagePositionReturning(
conversationId = CONVERSATION_ID,
messageId = MESSAGE_ID,
response = Either.Left(StorageFailure.DataNotFound)
)
.arrange()

getSearchedConversationMessagePosition(
conversationId = CONVERSATION_ID,
messageId = MESSAGE_ID
)

verify(arrangement.messageRepository)
.coroutine {
arrangement.messageRepository.getSearchedConversationMessagePosition(
conversationId = CONVERSATION_ID,
messageId = MESSAGE_ID
)
}
.wasInvoked(exactly = once)
}

@Test
fun givenRepositoryFails_whenInvokingUseCase_thenShouldPropagateTheFailure() = runTest {
val cause = StorageFailure.DataNotFound
val (_, getSearchedConversationMessagePosition) = Arrangement()
.withRepositoryMessagePositionReturning(
conversationId = CONVERSATION_ID,
messageId = MESSAGE_ID,
response = Either.Left(StorageFailure.DataNotFound)
)
.arrange()

val result = getSearchedConversationMessagePosition(
conversationId = CONVERSATION_ID,
messageId = MESSAGE_ID
)

assertIs<GetSearchedConversationMessagePositionUseCase.Result.Failure>(result)
assertEquals(cause, result.cause)
}

@Test
fun givenRepositorySucceeds_whenInvokingUseCase_thenShouldPropagateTheSuccess() = runTest {
val expectedMessagePosition = 113
val (_, getSearchedConversationMessagePosition) = Arrangement()
.withRepositoryMessagePositionReturning(
conversationId = CONVERSATION_ID,
messageId = MESSAGE_ID,
response = Either.Right(expectedMessagePosition)
)
.arrange()

val result = getSearchedConversationMessagePosition(
conversationId = CONVERSATION_ID,
messageId = MESSAGE_ID
)

assertIs<GetSearchedConversationMessagePositionUseCase.Result.Success>(result)
assertEquals(expectedMessagePosition, result.position)
}

private inner class Arrangement {

@Mock
val messageRepository: MessageRepository = mock(MessageRepository::class)

private val getSearchedConversationMessagePosition by lazy {
GetSearchedConversationMessagePositionUseCaseImpl(messageRepository = messageRepository)
}

suspend fun withRepositoryMessagePositionReturning(
conversationId: ConversationId,
messageId: String,
response: Either<StorageFailure, Int>
) = apply {
given(messageRepository)
.coroutine {
messageRepository.getSearchedConversationMessagePosition(
conversationId = conversationId,
messageId = messageId
)
}
.thenReturn(response)
}

fun arrange() = this to getSearchedConversationMessagePosition
}

private companion object {
const val MESSAGE_ID = TestMessage.TEST_MESSAGE_ID
val MESSAGE = TestMessage.TEXT_MESSAGE
val CONVERSATION_ID = TestConversation.ID
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,8 @@ class MessageExtensionsTest : BaseDatabaseTest() {
private fun getPager(): KaliumPager<MessageEntity> = messageExtensions.getPagerForConversation(
conversationId = CONVERSATION_ID,
visibilities = MessageEntity.Visibility.values().toList(),
pagingConfig = PagingConfig(PAGE_SIZE)
pagingConfig = PagingConfig(PAGE_SIZE),
startingOffset = 0
)

private suspend fun PagingSource<Int, MessageEntity>.refresh() = load(
Expand Down
Loading

0 comments on commit eb47fb6

Please sign in to comment.