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: add and remove conversation favorite [WPB-11639] #3119

Merged
merged 7 commits into from
Nov 25, 2024
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 @@ -293,14 +293,16 @@ sealed class ConversationDetails(open val conversation: Conversation) {
override val conversation: Conversation,
val otherUser: OtherUser,
val userType: UserType,
val isFavorite: Boolean = false
) : ConversationDetails(conversation)

data class Group(
override val conversation: Conversation,
val hasOngoingCall: Boolean = false,
val isSelfUserMember: Boolean,
val isSelfUserCreator: Boolean,
val selfRole: Conversation.Member.Role?
val selfRole: Conversation.Member.Role?,
val isFavorite: Boolean = false
// val isTeamAdmin: Boolean, TODO kubaz
) : ConversationDetails(conversation)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,7 @@ internal class ConversationMapperImpl(
activeOneOnOneConversationId = userActiveOneOnOneConversationId?.toModel()
),
userType = domainUserTypeMapper.fromUserTypeEntity(userType),
isFavorite = isFavorite
)
}

Expand All @@ -272,7 +273,8 @@ internal class ConversationMapperImpl(
hasOngoingCall = callStatus != null, // todo: we can do better!
isSelfUserMember = isMember,
isSelfUserCreator = isCreator == 1L,
selfRole = selfRole?.let { conversationRoleMapper.fromDAO(it) }
selfRole = selfRole?.let { conversationRoleMapper.fromDAO(it) },
isFavorite = isFavorite
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,13 @@ fun ConversationFolderEntity.toModel() = ConversationFolder(
type = type.toModel()
)

fun FolderWithConversationsEntity.toModel() = FolderWithConversations(
id = id,
name = name,
type = type.toModel(),
conversationIdList = conversationIdList.map { it.toModel() }
)

fun FolderWithConversations.toDao() = FolderWithConversationsEntity(
id = id,
name = name,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package com.wire.kalium.logic.data.conversation.folders

import com.benasher44.uuid.uuid4
import com.wire.kalium.logger.KaliumLogger.Companion.ApplicationFlow.CONVERSATIONS_FOLDERS
import com.wire.kalium.logger.obfuscateId
import com.wire.kalium.logic.CoreFailure
import com.wire.kalium.logic.NetworkFailure
import com.wire.kalium.logic.data.conversation.ConversationDetailsWithEvents
Expand All @@ -27,8 +28,10 @@ import com.wire.kalium.logic.data.conversation.ConversationMapper
import com.wire.kalium.logic.data.conversation.FolderType
import com.wire.kalium.logic.data.conversation.FolderWithConversations
import com.wire.kalium.logic.data.id.QualifiedID
import com.wire.kalium.logic.data.id.toDao
import com.wire.kalium.logic.di.MapperProvider
import com.wire.kalium.logic.functional.Either
import com.wire.kalium.logic.functional.flatMap
import com.wire.kalium.logic.functional.flatMapLeft
import com.wire.kalium.logic.functional.map
import com.wire.kalium.logic.functional.onFailure
Expand All @@ -51,6 +54,9 @@ internal interface ConversationFolderRepository {
suspend fun observeConversationsFromFolder(folderId: String): Flow<List<ConversationDetailsWithEvents>>
suspend fun updateConversationFolders(folderWithConversations: List<FolderWithConversations>): Either<CoreFailure, Unit>
suspend fun fetchConversationFolders(): Either<CoreFailure, Unit>
suspend fun addConversationToFolder(conversationId: QualifiedID, folderId: String): Either<CoreFailure, Unit>
suspend fun removeConversationFromFolder(conversationId: QualifiedID, folderId: String): Either<CoreFailure, Unit>
suspend fun syncConversationFoldersFromLocal(): Either<CoreFailure, Unit>
}

internal class ConversationFolderDataSource internal constructor(
Expand Down Expand Up @@ -119,4 +125,31 @@ internal class ConversationFolderDataSource internal constructor(
}
.map { }

override suspend fun addConversationToFolder(conversationId: QualifiedID, folderId: String): Either<CoreFailure, Unit> {
kaliumLogger.withFeatureId(CONVERSATIONS_FOLDERS)
.v("Adding conversation ${conversationId.toLogString()} to folder ${folderId.obfuscateId()}")
return wrapStorageRequest {
conversationFolderDAO.addConversationToFolder(conversationId.toDao(), folderId)
}
}

override suspend fun removeConversationFromFolder(conversationId: QualifiedID, folderId: String): Either<CoreFailure, Unit> {
kaliumLogger.withFeatureId(CONVERSATIONS_FOLDERS)
.v("Removing conversation ${conversationId.toLogString()} from folder ${folderId.obfuscateId()}")
return wrapStorageRequest {
conversationFolderDAO.removeConversationFromFolder(conversationId.toDao(), folderId)
}
}

override suspend fun syncConversationFoldersFromLocal(): Either<CoreFailure, Unit> {
kaliumLogger.withFeatureId(CONVERSATIONS_FOLDERS).v("Syncing conversation folders from local")
return wrapStorageRequest { conversationFolderDAO.getFoldersWithConversations().map { it.toModel() } }
.flatMap {
wrapApiRequest {
userPropertiesApi.updateLabels(
LabelListResponseDTO(it.map { it.toLabel() })
)
}
}
}
Comment on lines +144 to +154
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is also breaking the separation of concerns.

We can have this logic in a use case, as it's responsible for orchestrating business logic.

}
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,14 @@ import com.wire.kalium.logic.feature.connection.MarkConnectionRequestAsNotifiedU
import com.wire.kalium.logic.feature.connection.ObserveConnectionListUseCase
import com.wire.kalium.logic.feature.connection.ObservePendingConnectionRequestsUseCase
import com.wire.kalium.logic.feature.connection.ObservePendingConnectionRequestsUseCaseImpl
import com.wire.kalium.logic.feature.conversation.folder.AddConversationToFavoritesUseCase
import com.wire.kalium.logic.feature.conversation.folder.AddConversationToFavoritesUseCaseImpl
import com.wire.kalium.logic.feature.conversation.folder.GetFavoriteFolderUseCase
import com.wire.kalium.logic.feature.conversation.folder.GetFavoriteFolderUseCaseImpl
import com.wire.kalium.logic.feature.conversation.folder.ObserveConversationsFromFolderUseCase
import com.wire.kalium.logic.feature.conversation.folder.ObserveConversationsFromFolderUseCaseImpl
import com.wire.kalium.logic.feature.conversation.folder.RemoveConversationFromFavoritesUseCase
import com.wire.kalium.logic.feature.conversation.folder.RemoveConversationFromFavoritesUseCaseImpl
import com.wire.kalium.logic.feature.conversation.guestroomlink.CanCreatePasswordProtectedLinksUseCase
import com.wire.kalium.logic.feature.conversation.guestroomlink.GenerateGuestRoomLinkUseCase
import com.wire.kalium.logic.feature.conversation.guestroomlink.GenerateGuestRoomLinkUseCaseImpl
Expand Down Expand Up @@ -353,5 +357,8 @@ class ConversationScope internal constructor(
get() = ObserveConversationsFromFolderUseCaseImpl(conversationFolderRepository)
val getFavoriteFolder: GetFavoriteFolderUseCase
get() = GetFavoriteFolderUseCaseImpl(conversationFolderRepository)

val addConversationToFavorites: AddConversationToFavoritesUseCase
get() = AddConversationToFavoritesUseCaseImpl(conversationFolderRepository)
val removeConversationFromFavorites: RemoveConversationFromFavoritesUseCase
get() = RemoveConversationFromFavoritesUseCaseImpl(conversationFolderRepository)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* 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.conversation.folder

import com.wire.kalium.logic.CoreFailure
import com.wire.kalium.logic.data.conversation.folders.ConversationFolderRepository
import com.wire.kalium.logic.data.id.ConversationId
import com.wire.kalium.logic.functional.flatMap
import com.wire.kalium.logic.functional.fold
import com.wire.kalium.util.KaliumDispatcher
import com.wire.kalium.util.KaliumDispatcherImpl
import kotlinx.coroutines.withContext

/**
* This use case will add a conversation to the favorites folder.
*/
interface AddConversationToFavoritesUseCase {
/**
* @param conversationId the id of the conversation
* @return the [Result] indicating a successful operation, otherwise a [CoreFailure]
*/
suspend operator fun invoke(conversationId: ConversationId): Result

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

internal class AddConversationToFavoritesUseCaseImpl(
private val conversationFolderRepository: ConversationFolderRepository,
private val dispatchers: KaliumDispatcher = KaliumDispatcherImpl
) : AddConversationToFavoritesUseCase {
override suspend fun invoke(
conversationId: ConversationId
): AddConversationToFavoritesUseCase.Result = withContext(dispatchers.io) {
conversationFolderRepository.getFavoriteConversationFolder().fold(
{ AddConversationToFavoritesUseCase.Result.Failure(it) },
{ folder ->
conversationFolderRepository.addConversationToFolder(
conversationId,
folder.id
)
.flatMap {
conversationFolderRepository.syncConversationFoldersFromLocal()
}
.fold({
AddConversationToFavoritesUseCase.Result.Failure(it)
}, {
AddConversationToFavoritesUseCase.Result.Success
})
}
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* 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.conversation.folder

import com.wire.kalium.logic.CoreFailure
import com.wire.kalium.logic.data.conversation.folders.ConversationFolderRepository
import com.wire.kalium.logic.data.id.ConversationId
import com.wire.kalium.logic.functional.flatMap
import com.wire.kalium.logic.functional.fold
import com.wire.kalium.util.KaliumDispatcher
import com.wire.kalium.util.KaliumDispatcherImpl
import kotlinx.coroutines.withContext

/**
* This use case will remove a conversation from the favorites folder.
*/
interface RemoveConversationFromFavoritesUseCase {
/**
* @param conversationId the id of the conversation
* @return the [Result] indicating a successful operation, otherwise a [CoreFailure]
*/
suspend operator fun invoke(conversationId: ConversationId): Result

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

internal class RemoveConversationFromFavoritesUseCaseImpl(
private val conversationFolderRepository: ConversationFolderRepository,
private val dispatchers: KaliumDispatcher = KaliumDispatcherImpl
) : RemoveConversationFromFavoritesUseCase {
override suspend fun invoke(
conversationId: ConversationId
): RemoveConversationFromFavoritesUseCase.Result = withContext(dispatchers.io) {
conversationFolderRepository.getFavoriteConversationFolder().fold(
{ RemoveConversationFromFavoritesUseCase.Result.Failure(it) },
{ folder ->
conversationFolderRepository.removeConversationFromFolder(conversationId, folder.id)
.flatMap {
conversationFolderRepository.syncConversationFoldersFromLocal()
}
.fold({
RemoveConversationFromFavoritesUseCase.Result.Failure(it)
}, {
RemoveConversationFromFavoritesUseCase.Result.Success
})
}
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ internal class SlowSyncManager(
* Useful when a new step is added to Slow Sync, or when we fix some bug in Slow Sync,
* and we'd like to get all users to take advantage of the fix.
*/
const val CURRENT_VERSION = 8
const val CURRENT_VERSION = 9

val MIN_RETRY_DELAY = 1.seconds
val MAX_RETRY_DELAY = 10.minutes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ 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.data.id.toDao
import com.wire.kalium.logic.di.MapperProvider
import com.wire.kalium.logic.framework.TestConversation
import com.wire.kalium.logic.framework.TestUser
Expand Down Expand Up @@ -155,6 +156,66 @@ class ConversationFolderRepositoryTest {
coVerify { arrangement.conversationFolderDAO.updateConversationFolders(any()) }.wasInvoked()
}

@Test
fun givenValidConversationAndFolderWhenAddingConversationThenShouldAddSuccessfully() = runTest {
// given
val folderId = "folder1"
val conversationId = TestConversation.ID
val arrangement = Arrangement()
.withAddConversationToFolder()
.withGetFoldersWithConversations()
.withUpdateLabels(NetworkResponse.Success(Unit, mapOf(), 200))

// when
val result = arrangement.repository.addConversationToFolder(conversationId, folderId)

// then
result.shouldSucceed()
coVerify { arrangement.conversationFolderDAO.addConversationToFolder(eq(conversationId.toDao()), eq(folderId)) }.wasInvoked()
}

@Test
fun givenValidConversationAndFolderWhenRemovingConversationThenShouldRemoveSuccessfully() = runTest {
// given
val folderId = "folder1"
val conversationId = TestConversation.ID
val arrangement = Arrangement()
.withRemoveConversationFromFolder()
.withGetFoldersWithConversations()
.withUpdateLabels(NetworkResponse.Success(Unit, mapOf(), 200))

// when
val result = arrangement.repository.removeConversationFromFolder(conversationId, folderId)

// then
result.shouldSucceed()
coVerify { arrangement.conversationFolderDAO.removeConversationFromFolder(eq(conversationId.toDao()), eq(folderId)) }.wasInvoked()
}

@Test
fun givenLocalFoldersWhenSyncingFoldersThenShouldUpdateSuccessfully() = runTest {
// given
val folders = listOf(
FolderWithConversations(
id = "folder1",
name = "Favorites",
type = FolderType.FAVORITE,
conversationIdList = emptyList()
)
)
val arrangement = Arrangement()
.withGetFoldersWithConversations(folders)
.withUpdateLabels(NetworkResponse.Success(Unit, mapOf(), 200))

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

// then
result.shouldSucceed()
coVerify { arrangement.userPropertiesApi.updateLabels(any()) }.wasInvoked()
coVerify { arrangement.conversationFolderDAO.getFoldersWithConversations() }.wasInvoked()
}

private class Arrangement {

@Mock
Expand Down Expand Up @@ -197,5 +258,25 @@ class ConversationFolderRepositoryTest {
coEvery { userPropertiesApi.setProperty(any(), any()) }.returns(response)
return this
}

suspend fun withUpdateLabels(response: NetworkResponse<Unit>): Arrangement {
coEvery { userPropertiesApi.updateLabels(any()) }.returns(response)
return this
}

suspend fun withGetFoldersWithConversations(folders: List<FolderWithConversations> = emptyList()): Arrangement {
coEvery { conversationFolderDAO.getFoldersWithConversations() }.returns(folders.map { it.toDao() })
return this
}

suspend fun withAddConversationToFolder(): Arrangement {
coEvery { conversationFolderDAO.addConversationToFolder(any(), any()) }.returns(Unit)
return this
}

suspend fun withRemoveConversationFromFolder(): Arrangement {
coEvery { conversationFolderDAO.removeConversationFromFolder(any(), any()) }.returns(Unit)
return this
}
}
}
Loading
Loading