Skip to content

Commit

Permalink
feat: conversation folders [WPB-14309] (#3106)
Browse files Browse the repository at this point in the history
* feat: conversation folders

* feat: conversation folders

* fix: labels converersion

* detekt fix

* fix: handle favorite conversations

* remove unnecessary prints

* detekt fix

* review fixes

* detekt fix

* tests fix

* test fix

* renamed tests
  • Loading branch information
Garzas authored Nov 15, 2024
1 parent f2ae88b commit d140d79
Show file tree
Hide file tree
Showing 46 changed files with 1,166 additions and 90 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* 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

import com.wire.kalium.logic.data.id.QualifiedID

data class ConversationFolder(
val id: String,
val name: String,
val type: FolderType
)

data class FolderWithConversations(
val id: String,
val name: String,
val type: FolderType,
val conversationIdList: List<QualifiedID>
)

enum class FolderType {
USER,
FAVORITE,
}
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ class KaliumLogger(

enum class ApplicationFlow {
SYNC, EVENT_RECEIVER, CONVERSATIONS, CONNECTIONS, MESSAGES, SEARCH, SESSION, REGISTER,
CLIENTS, CALLING, ASSETS, LOCAL_STORAGE, ANALYTICS
CLIENTS, CALLING, ASSETS, LOCAL_STORAGE, ANALYTICS, CONVERSATIONS_FOLDERS
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,7 @@ internal class ConversationMapperImpl(
daoModel.lastMessage != null -> messageMapper.fromEntityToMessagePreview(daoModel.lastMessage!!)
else -> null
},
hasNewActivitiesToShow = daoModel.hasNewActivitiesToShow
)

override fun fromDaoModel(daoModel: ProposalTimerEntity): ProposalTimer =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ import com.wire.kalium.logic.data.id.toApi
import com.wire.kalium.logic.data.id.toCrypto
import com.wire.kalium.logic.data.id.toDao
import com.wire.kalium.logic.data.id.toModel
import com.wire.kalium.logic.data.message.MessageMapper
import com.wire.kalium.logic.data.message.SelfDeletionTimer
import com.wire.kalium.logic.data.user.UserId
import com.wire.kalium.logic.di.MapperProvider
Expand Down Expand Up @@ -131,8 +130,16 @@ 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 observeConversationListDetails(
fromArchive: Boolean,
conversationFilter: ConversationFilter = ConversationFilter.ALL
): Flow<List<ConversationDetails>>

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 @@ -328,11 +335,10 @@ internal class ConversationDataSource internal constructor(
private val conversationStatusMapper: ConversationStatusMapper = MapperProvider.conversationStatusMapper(),
private val conversationRoleMapper: ConversationRoleMapper = MapperProvider.conversationRoleMapper(),
private val protocolInfoMapper: ProtocolInfoMapper = MapperProvider.protocolInfoMapper(),
private val messageMapper: MessageMapper = MapperProvider.messageMapper(selfUserId),
private val receiptModeMapper: ReceiptModeMapper = MapperProvider.receiptModeMapper()
) : ConversationRepository {
override val extensions: ConversationRepositoryExtensions =
ConversationRepositoryExtensionsImpl(conversationDAO, conversationMapper, messageMapper)
ConversationRepositoryExtensionsImpl(conversationDAO, conversationMapper)

// region Get/Observe by id

Expand All @@ -353,6 +359,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 @@ -517,14 +524,20 @@ internal class ConversationDataSource internal constructor(
return conversationDAO.getAllConversations().map { it.map(conversationMapper::fromDaoModel) }
}

override suspend fun observeConversationListDetails(fromArchive: Boolean): Flow<List<ConversationDetails>> =
conversationDAO.getAllConversationDetails(fromArchive).map { conversationViewEntityList ->
override suspend fun observeConversationListDetails(
fromArchive: Boolean,
conversationFilter: ConversationFilter
): Flow<List<ConversationDetails>> =
conversationDAO.getAllConversationDetails(fromArchive, conversationFilter.toDao()).map { conversationViewEntityList ->
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 @@ -20,13 +20,10 @@ package com.wire.kalium.logic.data.conversation
import app.cash.paging.PagingConfig
import app.cash.paging.PagingData
import app.cash.paging.map
import com.wire.kalium.logic.data.message.MessageMapper
import com.wire.kalium.logic.data.message.UnreadEventType
import com.wire.kalium.persistence.dao.conversation.ConversationDAO
import com.wire.kalium.persistence.dao.conversation.ConversationDetailsWithEventsEntity
import com.wire.kalium.persistence.dao.conversation.ConversationExtensions.QueryConfig
import com.wire.kalium.persistence.dao.message.KaliumPager
import com.wire.kalium.persistence.dao.unread.UnreadEventTypeEntity
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map

Expand All @@ -40,8 +37,7 @@ interface ConversationRepositoryExtensions {

class ConversationRepositoryExtensionsImpl internal constructor(
private val conversationDAO: ConversationDAO,
private val conversationMapper: ConversationMapper,
private val messageMapper: MessageMapper,
private val conversationMapper: ConversationMapper
) : ConversationRepositoryExtensions {
override suspend fun getPaginatedConversationDetailsWithEventsBySearchQuery(
queryConfig: ConversationQueryConfig,
Expand All @@ -61,27 +57,13 @@ class ConversationRepositoryExtensionsImpl internal constructor(
)
}

return pager.pagingDataFlow.map {
it.map {
ConversationDetailsWithEvents(
conversationDetails = conversationMapper.fromDaoModelToDetails(it.conversationViewEntity),
lastMessage = when {
it.messageDraft != null -> messageMapper.fromDraftToMessagePreview(it.messageDraft!!)
it.lastMessage != null -> messageMapper.fromEntityToMessagePreview(it.lastMessage!!)
else -> null
},
unreadEventCount = it.unreadEvents.unreadEvents.mapKeys {
when (it.key) {
UnreadEventTypeEntity.KNOCK -> UnreadEventType.KNOCK
UnreadEventTypeEntity.MISSED_CALL -> UnreadEventType.MISSED_CALL
UnreadEventTypeEntity.MENTION -> UnreadEventType.MENTION
UnreadEventTypeEntity.REPLY -> UnreadEventType.REPLY
UnreadEventTypeEntity.MESSAGE -> UnreadEventType.MESSAGE
}
},
hasNewActivitiesToShow = it.hasNewActivitiesToShow,
)
}
return pager.pagingDataFlow.map { pagingData ->
pagingData
.map { conversationDetailsWithEventsEntity ->
conversationMapper.fromDaoModelToDetailsWithEvents(
conversationDetailsWithEventsEntity
)
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* 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.data.conversation.ConversationFolder
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.data.id.toModel
import com.wire.kalium.network.api.authenticated.properties.LabelDTO
import com.wire.kalium.network.api.authenticated.properties.LabelTypeDTO
import com.wire.kalium.persistence.dao.conversation.folder.ConversationFolderEntity
import com.wire.kalium.persistence.dao.conversation.folder.ConversationFolderTypeEntity
import com.wire.kalium.persistence.dao.conversation.folder.FolderWithConversationsEntity

fun LabelDTO.toFolder(selfDomain: String) = FolderWithConversations(
conversationIdList = qualifiedConversations?.map { it.toModel() } ?: conversations.map { QualifiedID(it, selfDomain) },
id = id,
name = name,
type = type.toFolderType()
)

fun LabelTypeDTO.toFolderType() = when (this) {
LabelTypeDTO.USER -> FolderType.USER
LabelTypeDTO.FAVORITE -> FolderType.FAVORITE
}

fun ConversationFolderEntity.toModel() = ConversationFolder(
id = id,
name = name,
type = type.toModel()
)

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

fun FolderType.toDao() = when (this) {
FolderType.USER -> ConversationFolderTypeEntity.USER
FolderType.FAVORITE -> ConversationFolderTypeEntity.FAVORITE
}

fun ConversationFolderTypeEntity.toModel(): FolderType = when (this) {
ConversationFolderTypeEntity.USER -> FolderType.USER
ConversationFolderTypeEntity.FAVORITE -> FolderType.FAVORITE
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/*
* 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.benasher44.uuid.uuid4
import com.wire.kalium.logger.KaliumLogger.Companion.ApplicationFlow.CONVERSATIONS_FOLDERS
import com.wire.kalium.logic.CoreFailure
import com.wire.kalium.logic.data.conversation.ConversationDetailsWithEvents
import com.wire.kalium.logic.data.conversation.ConversationFolder
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.di.MapperProvider
import com.wire.kalium.logic.functional.Either
import com.wire.kalium.logic.functional.map
import com.wire.kalium.logic.functional.onFailure
import com.wire.kalium.logic.functional.onSuccess
import com.wire.kalium.logic.kaliumLogger
import com.wire.kalium.logic.wrapApiRequest
import com.wire.kalium.logic.wrapStorageRequest
import com.wire.kalium.network.api.base.authenticated.properties.PropertiesApi
import com.wire.kalium.persistence.dao.conversation.folder.ConversationFolderDAO
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map

internal interface ConversationFolderRepository {

suspend fun getFavoriteConversationFolder(): Either<CoreFailure, ConversationFolder>
suspend fun observeConversationsFromFolder(folderId: String): Flow<List<ConversationDetailsWithEvents>>
suspend fun updateConversationFolders(folderWithConversations: List<FolderWithConversations>): Either<CoreFailure, Unit>
suspend fun fetchConversationFolders(): Either<CoreFailure, Unit>
}

internal class ConversationFolderDataSource internal constructor(
private val conversationFolderDAO: ConversationFolderDAO,
private val userPropertiesApi: PropertiesApi,
private val selfUserId: QualifiedID,
private val conversationMapper: ConversationMapper = MapperProvider.conversationMapper(selfUserId)
) : ConversationFolderRepository {

override suspend fun updateConversationFolders(folderWithConversations: List<FolderWithConversations>): Either<CoreFailure, Unit> =
wrapStorageRequest {
conversationFolderDAO.updateConversationFolders(folderWithConversations.map { it.toDao() })
}

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

override suspend fun observeConversationsFromFolder(folderId: String): Flow<List<ConversationDetailsWithEvents>> =
conversationFolderDAO.observeConversationListFromFolder(folderId).map { conversationDetailsWithEventsEntityList ->
conversationDetailsWithEventsEntityList.map {
conversationMapper.fromDaoModelToDetailsWithEvents(it)
}
}

override suspend fun fetchConversationFolders(): Either<CoreFailure, Unit> = wrapApiRequest {
kaliumLogger.withFeatureId(CONVERSATIONS_FOLDERS).v("Fetching conversation folders")
userPropertiesApi.getLabels()
}
.onSuccess { labelsResponse ->
val folders = labelsResponse.labels.map { it.toFolder(selfUserId.domain) }.toMutableList()
val favoriteLabel = folders.firstOrNull { it.type == FolderType.FAVORITE }

if (favoriteLabel == null) {
kaliumLogger.withFeatureId(CONVERSATIONS_FOLDERS).v("Favorite label not found, creating a new one")
folders.add(
FolderWithConversations(
id = uuid4().toString(),
name = "", // name will be handled by localization
type = FolderType.FAVORITE,
conversationIdList = emptyList()
)
)
}
conversationFolderDAO.updateConversationFolders(folders.map { it.toDao() })
}
.onFailure {
kaliumLogger.withFeatureId(CONVERSATIONS_FOLDERS).e("Error fetching conversation folders $it")
Either.Left(it)
}
.map { }

}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import com.wire.kalium.logic.data.conversation.Conversation.Member
import com.wire.kalium.logic.data.conversation.Conversation.Protocol
import com.wire.kalium.logic.data.conversation.Conversation.ReceiptMode
import com.wire.kalium.logic.data.conversation.Conversation.TypingIndicatorMode
import com.wire.kalium.logic.data.conversation.FolderWithConversations
import com.wire.kalium.logic.data.conversation.MutedConversationStatus
import com.wire.kalium.logic.data.featureConfig.AppLockModel
import com.wire.kalium.logic.data.featureConfig.ClassifiedDomainsModel
Expand Down Expand Up @@ -708,6 +709,17 @@ sealed class Event(open val id: String) {
"value" to "$value"
)
}

data class FoldersUpdate(
override val id: String,
val folders: List<FolderWithConversations>,
) : UserProperty(id) {
override fun toLogMap(): Map<String, Any?> = mapOf(
typeKey to "User.UserProperty.FoldersUpdate",
idKey to id.obfuscateId(),
"folders" to folders.map { it.id.obfuscateId() }
)
}
}

data class Unknown(
Expand Down
Loading

0 comments on commit d140d79

Please sign in to comment.