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 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
@@ -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>
saleniuk marked this conversation as resolved.
Show resolved Hide resolved
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
Loading