Skip to content

Commit

Permalink
feat: add and remove conversation favorite [WPB-11639] (#3119)
Browse files Browse the repository at this point in the history
* feat: manage favorite folder

* feat: add and remove favorite folder

* remove unused error class

* review fixes
  • Loading branch information
Garzas authored Nov 25, 2024
1 parent e271934 commit c7b96a2
Show file tree
Hide file tree
Showing 28 changed files with 1,092 additions and 12 deletions.
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() })
)
}
}
}
}
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

0 comments on commit c7b96a2

Please sign in to comment.