Skip to content

Commit

Permalink
feat: GetNextAudioMessageInConversationUseCase WPB-11725 (#3191)
Browse files Browse the repository at this point in the history
* feat/add_get_sender_name_by_message_id_use_case

* Added unit test for new DB query

* Fixed test

* feat: GetNextAudioMessageInConversationUseCase

* Fixed code style
  • Loading branch information
borichellow authored Dec 23, 2024
1 parent 3c7855a commit ecdca03
Show file tree
Hide file tree
Showing 8 changed files with 262 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,7 @@ internal interface MessageRepository {
): Either<StorageFailure, AssetTransferStatus>

suspend fun getSenderNameByMessageId(conversationId: ConversationId, messageId: String): Either<CoreFailure, String>
suspend fun getNextAudioMessageInConversation(conversationId: ConversationId, messageId: String): Either<CoreFailure, String>
}

// TODO: suppress TooManyFunctions for now, something we need to fix in the future
Expand Down Expand Up @@ -711,4 +712,10 @@ internal class MessageDataSource internal constructor(

override suspend fun getSenderNameByMessageId(conversationId: ConversationId, messageId: String): Either<CoreFailure, String> =
wrapStorageRequest { messageDAO.getSenderNameById(messageId, conversationId.toDao()) }

override suspend fun getNextAudioMessageInConversation(
conversationId: ConversationId,
messageId: String
): Either<CoreFailure, String> =
wrapStorageRequest { messageDAO.getNextAudioMessageInConversation(messageId, conversationId.toDao()) }
}
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
Original file line number Diff line number Diff line change
@@ -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<GetMessageByIdUseCase.Result.Failure>(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<GetMessageByIdUseCase.Result.Success>(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<StorageFailure, Message>
) = 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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Original file line number Diff line number Diff line change
Expand Up @@ -162,4 +162,5 @@ interface MessageDAO {
suspend fun observeAssetStatuses(conversationId: QualifiedIDEntity): Flow<List<MessageAssetStatusEntity>>
suspend fun getMessageAssetTransferStatus(messageId: String, conversationId: QualifiedIDEntity): AssetTransferStatusEntity
suspend fun getSenderNameById(id: String, conversationId: QualifiedIDEntity): String?
suspend fun getNextAudioMessageInConversation(prevMessageId: String, conversationId: QualifiedIDEntity): String?
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)

}
Original file line number Diff line number Diff line change
Expand Up @@ -2351,7 +2351,7 @@ class MessageDAOTest : BaseDatabaseTest() {
val userInQuestion = userDetailsEntity1
val otherUser = userDetailsEntity2

val expectedMessages = listOf(
val insertingMessages = listOf(
newRegularMessageEntity(
"1",
conversationId = conversationEntity1.id,
Expand All @@ -2369,7 +2369,7 @@ class MessageDAOTest : BaseDatabaseTest() {
sender = otherUser
)
)
messageDAO.insertOrIgnoreMessages(expectedMessages)
messageDAO.insertOrIgnoreMessages(insertingMessages)

val result = messageDAO.getSenderNameById("1", conversationEntity1.id)

Expand All @@ -2380,7 +2380,7 @@ class MessageDAOTest : BaseDatabaseTest() {
fun givenMessagesAreInserted_whenGettingSenderNameByMessageId_thenOnlyRelevantNameReturned() = runTest {
insertInitialData()

val expectedMessages = listOf(
val insertingMessages = listOf(
newRegularMessageEntity(
"1",
conversationId = conversationEntity1.id,
Expand All @@ -2398,7 +2398,7 @@ class MessageDAOTest : BaseDatabaseTest() {
sender = userDetailsEntity2
)
)
messageDAO.insertOrIgnoreMessages(expectedMessages)
messageDAO.insertOrIgnoreMessages(insertingMessages)

val result = messageDAO.getSenderNameById("1", conversationEntity1.id)

Expand All @@ -2409,7 +2409,7 @@ class MessageDAOTest : BaseDatabaseTest() {
fun givenMessagesAreButNoUserInserted_whenGettingSenderNameByMessageId_thenNullNameReturned() = runTest {
insertInitialData()

val expectedMessages = listOf(
val insertingMessages = listOf(
newRegularMessageEntity(
"1",
conversationId = conversationEntity1.id,
Expand All @@ -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(
Expand Down Expand Up @@ -2479,4 +2499,54 @@ class MessageDAOTest : BaseDatabaseTest() {
),
visibility = if (isComplete) MessageEntity.Visibility.VISIBLE else MessageEntity.Visibility.HIDDEN
)

private fun listOfMessageWithAudioAssets(): List<MessageEntity> {
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
)
)
)
}
}

0 comments on commit ecdca03

Please sign in to comment.