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: GetNextAudioMessageInConversationUseCase WPB-11725 #3191

Merged
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 @@ -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
)
)
)
}
}
Loading