Skip to content

Commit

Permalink
feat: user folders [WPB-14442] (#3147)
Browse files Browse the repository at this point in the history
* feat: user folders

* detekt fix

* removed duplicated sq query

* fix: favorite folder error handling, non paginatated conversations from folder

* tests fix
  • Loading branch information
Garzas authored Dec 10, 2024
1 parent 46ab0bc commit 26b7d4b
Show file tree
Hide file tree
Showing 14 changed files with 151 additions and 39 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,22 @@
*/
package com.wire.kalium.logic.data.conversation

enum class ConversationFilter {
ALL,
FAVORITES,
GROUPS,
ONE_ON_ONE
import kotlinx.serialization.Serializable

@Serializable
sealed class ConversationFilter {
@Serializable
data object All : ConversationFilter()

@Serializable
data object Favorites : ConversationFilter()

@Serializable
data object Groups : ConversationFilter()

@Serializable
data object OneOnOne : ConversationFilter()

@Serializable
data class Folder(val folderName: String, val folderId: String) : ConversationFilter()
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,14 @@
package com.wire.kalium.logic.data.conversation

import com.wire.kalium.logic.data.id.QualifiedID
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class ConversationFolder(
val id: String,
val name: String,
val type: FolderType
@SerialName("id") val id: String,
@SerialName("name") val name: String,
@SerialName("folder_type") val type: FolderType
)

data class FolderWithConversations(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -676,15 +676,16 @@ internal fun ConversationEntity.VerificationStatus.toModel(): Conversation.Verif
}

internal fun ConversationFilter.toDao(): ConversationFilterEntity = when (this) {
ConversationFilter.ALL -> ConversationFilterEntity.ALL
ConversationFilter.FAVORITES -> ConversationFilterEntity.FAVORITES
ConversationFilter.GROUPS -> ConversationFilterEntity.GROUPS
ConversationFilter.ONE_ON_ONE -> ConversationFilterEntity.ONE_ON_ONE
ConversationFilter.All -> ConversationFilterEntity.ALL
ConversationFilter.Favorites -> ConversationFilterEntity.FAVORITES
ConversationFilter.Groups -> ConversationFilterEntity.GROUPS
ConversationFilter.OneOnOne -> ConversationFilterEntity.ONE_ON_ONE
is ConversationFilter.Folder -> ConversationFilterEntity.ALL // TODO think how to secure that
}

internal fun ConversationFilterEntity.toModel(): ConversationFilter = when (this) {
ConversationFilterEntity.ALL -> ConversationFilter.ALL
ConversationFilterEntity.FAVORITES -> ConversationFilter.FAVORITES
ConversationFilterEntity.GROUPS -> ConversationFilter.GROUPS
ConversationFilterEntity.ONE_ON_ONE -> ConversationFilter.ONE_ON_ONE
ConversationFilterEntity.ALL -> ConversationFilter.All
ConversationFilterEntity.FAVORITES -> ConversationFilter.Favorites
ConversationFilterEntity.GROUPS -> ConversationFilter.Groups
ConversationFilterEntity.ONE_ON_ONE -> ConversationFilter.OneOnOne
}
Original file line number Diff line number Diff line change
Expand Up @@ -132,12 +132,12 @@ interface ConversationRepository {
suspend fun observeConversationList(): Flow<List<Conversation>>
suspend fun observeConversationListDetails(
fromArchive: Boolean,
conversationFilter: ConversationFilter = ConversationFilter.ALL
conversationFilter: ConversationFilter = ConversationFilter.All
): Flow<List<ConversationDetails>>

suspend fun observeConversationListDetailsWithEvents(
fromArchive: Boolean = false,
conversationFilter: ConversationFilter = ConversationFilter.ALL
conversationFilter: ConversationFilter = ConversationFilter.All
): Flow<List<ConversationDetailsWithEvents>>

suspend fun getConversationIds(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,5 +73,5 @@ data class ConversationQueryConfig(
val fromArchive: Boolean = false,
val onlyInteractionEnabled: Boolean = false,
val newActivitiesOnTop: Boolean = false,
val conversationFilter: ConversationFilter = ConversationFilter.ALL,
val conversationFilter: ConversationFilter = ConversationFilter.All,
)
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ 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.mapRight
import com.wire.kalium.logic.functional.onFailure
import com.wire.kalium.logic.functional.onSuccess
import com.wire.kalium.logic.kaliumLogger
Expand All @@ -57,6 +58,7 @@ internal interface ConversationFolderRepository {
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>
suspend fun observeUserFolders(): Flow<Either<CoreFailure, List<ConversationFolder>>>
}

internal class ConversationFolderDataSource internal constructor(
Expand All @@ -72,7 +74,7 @@ internal class ConversationFolderDataSource internal constructor(
}

override suspend fun getFavoriteConversationFolder(): Either<CoreFailure, ConversationFolder> = wrapStorageRequest {
conversationFolderDAO.getFavoriteConversationFolder().toModel()
conversationFolderDAO.getFavoriteConversationFolder()?.toModel()
}

override suspend fun observeConversationsFromFolder(folderId: String): Flow<List<ConversationDetailsWithEvents>> =
Expand Down Expand Up @@ -152,4 +154,10 @@ internal class ConversationFolderDataSource internal constructor(
}
}
}

override suspend fun observeUserFolders(): Flow<Either<CoreFailure, List<ConversationFolder>>> {
return conversationFolderDAO.observeUserFolders()
.wrapStorageRequest()
.mapRight { folderEntities -> folderEntities.map { it.toModel() } }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ import com.wire.kalium.logic.feature.conversation.folder.GetFavoriteFolderUseCas
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.ObserveUserFoldersUseCase
import com.wire.kalium.logic.feature.conversation.folder.ObserveUserFoldersUseCaseImpl
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
Expand Down Expand Up @@ -361,4 +363,6 @@ class ConversationScope internal constructor(
get() = AddConversationToFavoritesUseCaseImpl(conversationFolderRepository)
val removeConversationFromFavorites: RemoveConversationFromFavoritesUseCase
get() = RemoveConversationFromFavoritesUseCaseImpl(conversationFolderRepository)
val observeUserFolders: ObserveUserFoldersUseCase
get() = ObserveUserFoldersUseCaseImpl(conversationFolderRepository)
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,16 +45,26 @@ internal class ObserveConversationListDetailsWithEventsUseCaseImpl(
fromArchive: Boolean,
conversationFilter: ConversationFilter
): Flow<List<ConversationDetailsWithEvents>> {
return if (conversationFilter == ConversationFilter.FAVORITES) {
when (val result = getFavoriteFolder()) {
GetFavoriteFolderUseCase.Result.Failure -> {
flowOf(emptyList())
return when (conversationFilter) {
ConversationFilter.Favorites -> {
when (val result = getFavoriteFolder()) {
GetFavoriteFolderUseCase.Result.Failure -> {
flowOf(emptyList())
}

is GetFavoriteFolderUseCase.Result.Success ->
conversationFolderRepository.observeConversationsFromFolder(result.folder.id)
}
}

is GetFavoriteFolderUseCase.Result.Success -> conversationFolderRepository.observeConversationsFromFolder(result.folder.id)
is ConversationFilter.Folder -> {
conversationFolderRepository.observeConversationsFromFolder(conversationFilter.folderId)
}
} else {
conversationRepository.observeConversationListDetailsWithEvents(fromArchive, conversationFilter)

ConversationFilter.All,
ConversationFilter.Groups,
ConversationFilter.OneOnOne ->
conversationRepository.observeConversationListDetailsWithEvents(fromArchive, conversationFilter)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ package com.wire.kalium.logic.feature.conversation.folder

import com.wire.kalium.logic.data.conversation.ConversationDetailsWithEvents
import com.wire.kalium.logic.data.conversation.folders.ConversationFolderRepository
import com.wire.kalium.util.KaliumDispatcher
import com.wire.kalium.util.KaliumDispatcherImpl
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOn

/**
* This use case will observe and return the list of conversations from given folder.
Expand All @@ -31,9 +34,10 @@ fun interface ObserveConversationsFromFolderUseCase {

internal class ObserveConversationsFromFolderUseCaseImpl(
private val conversationFolderRepository: ConversationFolderRepository,
private val dispatchers: KaliumDispatcher = KaliumDispatcherImpl
) : ObserveConversationsFromFolderUseCase {

override suspend operator fun invoke(folderId: String): Flow<List<ConversationDetailsWithEvents>> {
return conversationFolderRepository.observeConversationsFromFolder(folderId)
}
override suspend operator fun invoke(folderId: String): Flow<List<ConversationDetailsWithEvents>> =
conversationFolderRepository.observeConversationsFromFolder(folderId)
.flowOn(dispatchers.io)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* 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.data.conversation.ConversationFolder
import com.wire.kalium.logic.data.conversation.folders.ConversationFolderRepository
import com.wire.kalium.logic.functional.mapToRightOr
import com.wire.kalium.util.KaliumDispatcher
import com.wire.kalium.util.KaliumDispatcherImpl
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOn

/**
* This use case will observe and return the list of all user folders.
* @see ConversationFolder
*/
fun interface ObserveUserFoldersUseCase {
suspend operator fun invoke(): Flow<List<ConversationFolder>>
}

internal class ObserveUserFoldersUseCaseImpl(
private val conversationFolderRepository: ConversationFolderRepository,
private val dispatchers: KaliumDispatcher = KaliumDispatcherImpl
) : ObserveUserFoldersUseCase {

override suspend operator fun invoke(): Flow<List<ConversationFolder>> {
return conversationFolderRepository.observeUserFolders()
.mapToRightOr(emptyList())
.flowOn(dispatchers.io)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ CREATE TABLE LabeledConversation (
PRIMARY KEY (folder_id, conversation_id)
);

getUserFolders:
SELECT * FROM ConversationFolder
WHERE folder_type != 'FAVORITE';

getAllFoldersWithConversations:
SELECT
conversationFolder.id AS label_id,
Expand Down Expand Up @@ -60,8 +64,8 @@ VALUES(?, ?);
deleteLabeledConversation:
DELETE FROM LabeledConversation WHERE conversation_id = ? AND folder_id = ?;

clearFolders:
DELETE FROM ConversationFolder;

clearLabeledConversations:
DELETE FROM LabeledConversation;

clearFolders:
DELETE FROM ConversationFolder;
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,9 @@ import kotlinx.coroutines.flow.Flow
interface ConversationFolderDAO {
suspend fun getFoldersWithConversations(): List<FolderWithConversationsEntity>
suspend fun observeConversationListFromFolder(folderId: String): Flow<List<ConversationDetailsWithEventsEntity>>
suspend fun getFavoriteConversationFolder(): ConversationFolderEntity
suspend fun getFavoriteConversationFolder(): ConversationFolderEntity?
suspend fun updateConversationFolders(folderWithConversationsList: List<FolderWithConversationsEntity>)
suspend fun addConversationToFolder(conversationId: QualifiedIDEntity, folderId: String)
suspend fun removeConversationFromFolder(conversationId: QualifiedIDEntity, folderId: String)
suspend fun observeUserFolders(): Flow<List<ConversationFolderEntity>>
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
package com.wire.kalium.persistence.dao.conversation.folder

import app.cash.sqldelight.coroutines.asFlow
import com.wire.kalium.persistence.ConversationFolder
import com.wire.kalium.persistence.ConversationFoldersQueries
import com.wire.kalium.persistence.GetAllFoldersWithConversations
import com.wire.kalium.persistence.dao.QualifiedIDEntity
Expand All @@ -26,6 +27,7 @@ import com.wire.kalium.persistence.dao.conversation.ConversationDetailsWithEvent
import com.wire.kalium.persistence.util.mapToList
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
import kotlin.coroutines.CoroutineContext

Expand All @@ -35,6 +37,14 @@ class ConversationFolderDAOImpl internal constructor(
) : ConversationFolderDAO {
private val conversationDetailsWithEventsMapper = ConversationDetailsWithEventsMapper

override suspend fun observeUserFolders(): Flow<List<ConversationFolderEntity>> {
return conversationFoldersQueries.getUserFolders()
.asFlow()
.mapToList()
.map { it.map(::toEntity) }
.flowOn(coroutineContext)
}

override suspend fun getFoldersWithConversations(): List<FolderWithConversationsEntity> = withContext(coroutineContext) {
val labeledConversationList = conversationFoldersQueries.getAllFoldersWithConversations().executeAsList().map(::toEntity)

Expand All @@ -59,6 +69,12 @@ class ConversationFolderDAOImpl internal constructor(
conversationId = row.conversation_id
)

private fun toEntity(row: ConversationFolder) = ConversationFolderEntity(
id = row.id,
name = row.name,
type = row.folder_type
)

override suspend fun observeConversationListFromFolder(folderId: String): Flow<List<ConversationDetailsWithEventsEntity>> {
return conversationFoldersQueries.getConversationsFromFolder(
folderId,
Expand All @@ -69,11 +85,11 @@ class ConversationFolderDAOImpl internal constructor(
.flowOn(coroutineContext)
}

override suspend fun getFavoriteConversationFolder(): ConversationFolderEntity {
override suspend fun getFavoriteConversationFolder(): ConversationFolderEntity? {
return conversationFoldersQueries.getFavoriteFolder { id, name, folderType ->
ConversationFolderEntity(id, name, folderType)
}
.executeAsOne()
.executeAsOneOrNull()
}

override suspend fun updateConversationFolders(folderWithConversationsList: List<FolderWithConversationsEntity>) =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ class ConversationFolderDAOTest : BaseDatabaseTest() {
id = folderId,
name = "folderName",
type = ConversationFolderTypeEntity.USER,
conversationIdList = listOf(conversationEntity1.id))
conversationIdList = listOf(conversationEntity1.id)
)

db.conversationFolderDAO.updateConversationFolders(listOf(conversationFolderEntity))
val result = db.conversationFolderDAO.observeConversationListFromFolder(folderId).first().first()
Expand All @@ -79,12 +80,13 @@ class ConversationFolderDAOTest : BaseDatabaseTest() {
id = folderId,
name = "",
type = ConversationFolderTypeEntity.FAVORITE,
conversationIdList = listOf(conversationEntity1.id))
conversationIdList = listOf(conversationEntity1.id)
)

db.conversationFolderDAO.updateConversationFolders(listOf(conversationFolderEntity))
val result = db.conversationFolderDAO.getFavoriteConversationFolder()

assertEquals(folderId, result.id)
assertEquals(folderId, result?.id)
}

@Test
Expand Down

0 comments on commit 26b7d4b

Please sign in to comment.