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: conversation folders [WPB-14309] #3106

Merged
merged 19 commits into from
Nov 15, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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 @@ -71,6 +71,7 @@ import com.wire.kalium.persistence.dao.client.ClientDAO
import com.wire.kalium.persistence.dao.conversation.ConversationDAO
import com.wire.kalium.persistence.dao.conversation.ConversationDetailsWithEventsEntity
import com.wire.kalium.persistence.dao.conversation.ConversationEntity
import com.wire.kalium.persistence.dao.conversation.ConversationFilterEntity
import com.wire.kalium.persistence.dao.conversation.ConversationMetaDataDAO
import com.wire.kalium.persistence.dao.member.MemberDAO
import com.wire.kalium.persistence.dao.message.MessageDAO
Expand Down Expand Up @@ -131,7 +132,11 @@ interface ConversationRepository {
suspend fun getConversationList(): Either<StorageFailure, Flow<List<Conversation>>>
suspend fun observeConversationList(): Flow<List<Conversation>>
suspend fun observeConversationListDetails(fromArchive: Boolean): Flow<List<ConversationDetails>>
suspend fun observeConversationListDetailsWithEvents(fromArchive: Boolean = false): Flow<List<ConversationDetailsWithEvents>>
suspend fun observeConversationListDetailsWithEvents(
fromArchive: Boolean = false,
conversationFilter: ConversationFilter = ConversationFilter.ALL
): Flow<List<ConversationDetailsWithEvents>>

suspend fun getConversationIds(
type: Conversation.Type,
protocol: Conversation.Protocol,
Expand Down Expand Up @@ -351,6 +356,7 @@ internal class ConversationDataSource internal constructor(
conversationMapper.fromConversationEntityType(it)
}
}

override suspend fun observeConversationDetailsById(conversationID: ConversationId): Flow<Either<StorageFailure, ConversationDetails>> =
conversationDAO.observeConversationDetailsById(conversationID.toDao())
.wrapStorageRequest()
Expand Down Expand Up @@ -516,13 +522,16 @@ internal class ConversationDataSource internal constructor(
}

override suspend fun observeConversationListDetails(fromArchive: Boolean): Flow<List<ConversationDetails>> =
conversationDAO.getAllConversationDetails(fromArchive).map { conversationViewEntityList ->
conversationDAO.getAllConversationDetails(fromArchive, ConversationFilterEntity.ALL).map { conversationViewEntityList ->
Garzas marked this conversation as resolved.
Show resolved Hide resolved
conversationViewEntityList.map { conversationViewEntity -> conversationMapper.fromDaoModelToDetails(conversationViewEntity) }
}

override suspend fun observeConversationListDetailsWithEvents(fromArchive: Boolean): Flow<List<ConversationDetailsWithEvents>> =
override suspend fun observeConversationListDetailsWithEvents(
fromArchive: Boolean,
conversationFilter: ConversationFilter
): Flow<List<ConversationDetailsWithEvents>> =
combine(
conversationDAO.getAllConversationDetails(fromArchive),
conversationDAO.getAllConversationDetails(fromArchive, conversationFilter.toDao()),
if (fromArchive) flowOf(listOf()) else messageDAO.observeLastMessages(),
messageDAO.observeConversationsUnreadEvents(),
messageDraftDAO.observeMessageDrafts()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ internal class ConversationFolderDataSource internal constructor(
}

override suspend fun observeConversationsFromFolder(folderId: String): Flow<List<ConversationDetailsWithEvents>> =
conversationFolderDAO.observerConversationFromFolder(folderId).map { conversationDetailsWithEventsEntityList ->
conversationFolderDAO.observeConversationListFromFolder(folderId).map { conversationDetailsWithEventsEntityList ->
conversationDetailsWithEventsEntityList.map {
conversationMapper.toModelConversationWithEvents(it)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ class ConversationScope internal constructor(
get() = ObserveConversationListDetailsUseCaseImpl(conversationRepository)

val observeConversationListDetailsWithEvents: ObserveConversationListDetailsWithEventsUseCase
get() = ObserveConversationListDetailsWithEventsUseCaseImpl(conversationRepository)
get() = ObserveConversationListDetailsWithEventsUseCaseImpl(conversationRepository, conversationFolderRepository, getFavoriteFolder)

val observeConversationMembers: ObserveConversationMembersUseCase
get() = ObserveConversationMembersUseCaseImpl(conversationRepository, userRepository)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,22 +20,43 @@ package com.wire.kalium.logic.feature.conversation

import com.wire.kalium.logic.data.conversation.ConversationDetails
import com.wire.kalium.logic.data.conversation.ConversationDetailsWithEvents
import com.wire.kalium.logic.data.conversation.ConversationFilter
import com.wire.kalium.logic.data.conversation.ConversationRepository
import com.wire.kalium.logic.data.conversation.folders.ConversationFolderRepository
import com.wire.kalium.logic.feature.conversation.folder.GetFavoriteFolderUseCase
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf

/**
* This use case will observe and return the list of conversation details for the current user.
* @see ConversationDetails
*/
fun interface ObserveConversationListDetailsWithEventsUseCase {
suspend operator fun invoke(fromArchive: Boolean): Flow<List<ConversationDetailsWithEvents>>
suspend operator fun invoke(fromArchive: Boolean, conversationFilter: ConversationFilter): Flow<List<ConversationDetailsWithEvents>>
}

internal class ObserveConversationListDetailsWithEventsUseCaseImpl(
private val conversationRepository: ConversationRepository,
private val conversationFolderRepository: ConversationFolderRepository,
private val getFavoriteFolder: GetFavoriteFolderUseCase
) : ObserveConversationListDetailsWithEventsUseCase {

override suspend operator fun invoke(fromArchive: Boolean): Flow<List<ConversationDetailsWithEvents>> {
return conversationRepository.observeConversationListDetailsWithEvents(fromArchive)
override suspend operator fun invoke(
fromArchive: Boolean,
conversationFilter: ConversationFilter
): Flow<List<ConversationDetailsWithEvents>> {
return if (conversationFilter == ConversationFilter.FAVORITES) {
println("KBX ObserveConversationListDetailsWithEventsUseCaseImpl invoke")
when (val result = getFavoriteFolder()) {
GetFavoriteFolderUseCase.Result.Failure -> {
println("KBX failure $result")
flowOf(listOf())
Garzas marked this conversation as resolved.
Show resolved Hide resolved
}

is GetFavoriteFolderUseCase.Result.Success -> conversationFolderRepository.observeConversationsFromFolder(result.folder.id)
}
} else {
conversationRepository.observeConversationListDetailsWithEvents(fromArchive, conversationFilter)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ class ConversationRepositoryExtensionsTest {
@Mock
private val messageMapper: MessageMapper = mock(MessageMapper::class)
private val conversationRepositoryExtensions: ConversationRepositoryExtensions by lazy {
ConversationRepositoryExtensionsImpl(conversationDAO, conversationMapper, messageMapper)
ConversationRepositoryExtensionsImpl(conversationDAO, conversationMapper)
}

init {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,8 @@ import com.wire.kalium.logic.util.arrangement.dao.MemberDAOArrangement
import com.wire.kalium.logic.util.arrangement.dao.MemberDAOArrangementImpl
import com.wire.kalium.logic.util.shouldFail
import com.wire.kalium.logic.util.shouldSucceed
import com.wire.kalium.network.api.base.authenticated.client.ClientApi
import com.wire.kalium.network.api.authenticated.conversation.ConvProtocol
import com.wire.kalium.network.api.authenticated.conversation.ConvProtocol.MLS
import com.wire.kalium.network.api.base.authenticated.conversation.ConversationApi
import com.wire.kalium.network.api.authenticated.conversation.ConversationMemberDTO
import com.wire.kalium.network.api.authenticated.conversation.ConversationMembersResponse
import com.wire.kalium.network.api.authenticated.conversation.ConversationNameUpdateEvent
Expand All @@ -69,6 +67,8 @@ import com.wire.kalium.network.api.authenticated.conversation.model.Conversation
import com.wire.kalium.network.api.authenticated.conversation.model.ConversationProtocolDTO
import com.wire.kalium.network.api.authenticated.conversation.model.ConversationReceiptModeDTO
import com.wire.kalium.network.api.authenticated.notification.EventContentDTO
import com.wire.kalium.network.api.base.authenticated.client.ClientApi
import com.wire.kalium.network.api.base.authenticated.conversation.ConversationApi
import com.wire.kalium.network.api.model.ConversationAccessDTO
import com.wire.kalium.network.api.model.ConversationAccessRoleDTO
import com.wire.kalium.network.exceptions.KaliumException
Expand All @@ -80,8 +80,8 @@ import com.wire.kalium.persistence.dao.client.ClientDAO
import com.wire.kalium.persistence.dao.client.ClientTypeEntity
import com.wire.kalium.persistence.dao.client.DeviceTypeEntity
import com.wire.kalium.persistence.dao.conversation.ConversationDAO
import com.wire.kalium.persistence.dao.conversation.ConversationDetailsWithEventsEntity
import com.wire.kalium.persistence.dao.conversation.ConversationEntity
import com.wire.kalium.persistence.dao.conversation.ConversationFilterEntity
import com.wire.kalium.persistence.dao.conversation.ConversationMetaDataDAO
import com.wire.kalium.persistence.dao.conversation.ConversationViewEntity
import com.wire.kalium.persistence.dao.message.MessageDAO
Expand All @@ -100,7 +100,6 @@ import io.mockative.any
import io.mockative.coEvery
import io.mockative.coVerify
import io.mockative.eq
import io.mockative.every
import io.mockative.fake.valueOf
import io.mockative.matchers.AnyMatcher
import io.mockative.matchers.EqualsMatcher
Expand All @@ -120,7 +119,6 @@ import kotlin.test.assertContains
import kotlin.test.assertEquals
import kotlin.test.assertIs
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
import com.wire.kalium.network.api.model.ConversationId as APIConversationId
import com.wire.kalium.persistence.dao.client.Client as ClientEntity

Expand Down Expand Up @@ -1516,7 +1514,7 @@ class ConversationRepositoryTest {

suspend fun withConversations(conversations: List<ConversationViewEntity>) = apply {
coEvery {
conversationDAO.getAllConversationDetails(any())
conversationDAO.getAllConversationDetails(any(), any())
}.returns(flowOf(conversations))
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
/*
* 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.data.conversation.folders

import com.wire.kalium.logic.NetworkFailure
import com.wire.kalium.logic.data.conversation.FolderType
import com.wire.kalium.logic.data.conversation.FolderWithConversations
import com.wire.kalium.logic.di.MapperProvider
import com.wire.kalium.logic.framework.TestConversation
import com.wire.kalium.logic.framework.TestUser
import com.wire.kalium.logic.util.shouldFail
import com.wire.kalium.logic.util.shouldSucceed
import com.wire.kalium.network.api.authenticated.properties.LabelListResponseDTO
import com.wire.kalium.network.api.base.authenticated.properties.PropertiesApi
import com.wire.kalium.network.exceptions.KaliumException
import com.wire.kalium.network.utils.NetworkResponse
import com.wire.kalium.persistence.dao.conversation.ConversationDetailsWithEventsEntity
import com.wire.kalium.persistence.dao.conversation.folder.ConversationFolderDAO
import com.wire.kalium.persistence.dao.conversation.folder.ConversationFolderEntity
import com.wire.kalium.persistence.dao.conversation.folder.ConversationFolderTypeEntity
import com.wire.kalium.persistence.dao.unread.ConversationUnreadEventEntity
import io.ktor.util.reflect.instanceOf
import io.mockative.Mock
import io.mockative.any
import io.mockative.coEvery
import io.mockative.coVerify
import io.mockative.mock
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
import kotlin.test.Test
import kotlin.test.assertEquals

class ConversationFolderRepositoryTest {

@Test
fun `given favorite folder exists when fetching favorite folder then should return folder successfully`() = runTest {
// given
val folder = ConversationFolderEntity(id = "folder1", name = "Favorites", type = ConversationFolderTypeEntity.FAVORITE)
val arrangement = Arrangement().withFavoriteConversationFolder(folder)

// when
val result = arrangement.repository.getFavoriteConversationFolder()

// then
result.shouldSucceed {
assertEquals(folder.toModel(), it)
}
coVerify { arrangement.conversationFolderDAO.getFavoriteConversationFolder() }.wasInvoked()
}

@Test
fun `given conversations in folder when observing conversations from folder then should emit conversations list`() = runTest {
// given
val folderId = "folder1"
val conversation = ConversationDetailsWithEventsEntity(
conversationViewEntity = TestConversation.VIEW_ENTITY,
lastMessage = null,
messageDraft = null,
unreadEvents = ConversationUnreadEventEntity(TestConversation.VIEW_ENTITY.id, mapOf()),
)

val conversations = listOf(conversation)
val arrangement = Arrangement().withConversationsFromFolder(folderId, conversations)

// when
val resultFlow = arrangement.repository.observeConversationsFromFolder(folderId)

// then
val emittedConversations = resultFlow.first()
assertEquals(arrangement.conversationMapper.toModelConversationWithEvents(conversations.first()), emittedConversations.first())
}

@Test
fun `given folder data when updating conversation folders then folders should be updated in database successfully`() = runTest {
// given
val folders = listOf(
FolderWithConversations(
id = "folder1", name = "Favorites", type = FolderType.FAVORITE,
conversationIdList = listOf()
)
)
val arrangement = Arrangement().withSuccessfulFolderUpdate()

// when
val result = arrangement.repository.updateConversationFolders(folders)

// then
result.shouldSucceed()
coVerify { arrangement.conversationFolderDAO.updateConversationFolders(any()) }.wasInvoked()
}

@Test
fun `given network failure when fetching conversation folders then should return network failure`() = runTest {
// given
val arrangement = Arrangement().withFetchConversationLabels(NetworkResponse.Error(KaliumException.NoNetwork()))

// when
val result = arrangement.repository.fetchConversationFolders()

// then
result.shouldFail { failure ->
failure.instanceOf(NetworkFailure.NoNetworkConnection::class)
}
}

private class Arrangement {

@Mock
val conversationFolderDAO = mock(ConversationFolderDAO::class)

@Mock
val userPropertiesApi = mock(PropertiesApi::class)

private val selfUserId = TestUser.SELF.id

val conversationMapper = MapperProvider.conversationMapper(selfUserId)

val repository = ConversationFolderDataSource(
conversationFolderDAO = conversationFolderDAO,
userPropertiesApi = userPropertiesApi,
selfUserId = selfUserId
)

suspend fun withFavoriteConversationFolder(folder: ConversationFolderEntity): Arrangement {
coEvery { conversationFolderDAO.getFavoriteConversationFolder() }.returns(folder)
return this
}

suspend fun withConversationsFromFolder(folderId: String, conversations: List<ConversationDetailsWithEventsEntity>): Arrangement {
coEvery { conversationFolderDAO.observeConversationListFromFolder(folderId) }.returns(flowOf(conversations))
return this
}

suspend fun withSuccessfulFolderUpdate(): Arrangement {
coEvery { conversationFolderDAO.updateConversationFolders(any()) }.returns(Unit)
return this
}

suspend fun withFetchConversationLabels(response: NetworkResponse<LabelListResponseDTO>): Arrangement {
coEvery { userPropertiesApi.getLabels() }.returns(response)
return this
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ package com.wire.kalium.logic.data.properties

import com.wire.kalium.logic.StorageFailure
import com.wire.kalium.logic.configuration.UserConfigRepository
import com.wire.kalium.logic.framework.TestUser
import com.wire.kalium.logic.functional.Either
import com.wire.kalium.logic.util.shouldSucceed
import com.wire.kalium.network.api.base.authenticated.properties.PropertiesApi
Expand Down Expand Up @@ -101,7 +102,9 @@ class UserPropertyRepositoryTest {
@Mock
val userConfigRepository = mock(UserConfigRepository::class)

private val userPropertyRepository = UserPropertyDataSource(propertiesApi, userConfigRepository)
private val selfUserId = TestUser.SELF.id

private val userPropertyRepository = UserPropertyDataSource(propertiesApi, userConfigRepository, selfUserId)

suspend fun withUpdateReadReceiptsSuccess() = apply {
coEvery {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import com.wire.kalium.cryptography.utils.EncryptedData
import com.wire.kalium.logic.data.conversation.ClientId
import com.wire.kalium.logic.data.conversation.Conversation
import com.wire.kalium.logic.data.conversation.Conversation.Member
import com.wire.kalium.logic.data.conversation.FolderType
import com.wire.kalium.logic.data.conversation.FolderWithConversations
import com.wire.kalium.logic.data.conversation.MutedConversationStatus
import com.wire.kalium.logic.data.event.Event
import com.wire.kalium.logic.data.event.EventDeliveryInfo
Expand All @@ -33,6 +35,8 @@ import com.wire.kalium.logic.data.user.Connection
import com.wire.kalium.logic.data.user.ConnectionState
import com.wire.kalium.logic.data.user.UserId
import com.wire.kalium.logic.sync.incremental.EventSource
import com.wire.kalium.persistence.dao.conversation.folder.ConversationFolderEntity
import com.wire.kalium.persistence.dao.conversation.folder.ConversationFolderTypeEntity
import com.wire.kalium.util.time.UNIX_FIRST_DATE
import io.ktor.util.encodeBase64
import kotlinx.datetime.Instant
Expand Down Expand Up @@ -167,6 +171,18 @@ object TestEvent {
value = true
)

fun foldersUpdate(eventId: String = "eventId") = Event.UserProperty.FoldersUpdate(
id = eventId,
folders = listOf(
FolderWithConversations(
id = "folder1",
name = "Favorites",
type = FolderType.FAVORITE,
conversationIdList = listOf(TestConversation.ID)
)
)
)

fun newMessageEvent(
encryptedContent: String,
senderUserId: UserId = TestUser.USER_ID,
Expand Down
Loading