From c471f6182c6d1835d306528646eda16ac73423e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Saleniuk?= Date: Wed, 9 Oct 2024 10:11:05 +0200 Subject: [PATCH 1/6] feat: conversation list pagination, pt1 - queries [WPB-9433] --- .../logic/data/conversation/Conversation.kt | 11 +- .../data/conversation/ConversationMapper.kt | 46 +- .../conversation/ConversationRepository.kt | 56 +-- .../wire/kalium/logic/di/MapperProvider.kt | 3 +- .../kalium/logic/feature/UserSessionScope.kt | 1 - .../logic/data/call/CallRepositoryTest.kt | 13 - .../conversation/ConversationMapperTest.kt | 78 +++- .../ConversationRepositoryTest.kt | 150 ++++--- .../EndCallOnConversationChangeUseCaseTest.kt | 4 - .../ObserveConversationDetailsUseCaseTest.kt | 4 - ...serveConversationListDetailsUseCaseTest.kt | 20 - .../framework/TestConversationDetails.kt | 4 - .../wire/kalium/persistence/Conversations.sq | 114 +++++ .../wire/kalium/persistence/MessagePreview.sq | 46 +- .../wire/kalium/persistence/UnreadEvents.sq | 22 +- .../src/commonMain/db_user/migrations/87.sqm | 129 ++++++ .../dao/conversation/ConversationDAO.kt | 5 + .../dao/conversation/ConversationDAOImpl.kt | 15 +- .../ConversationDetailsWithEventsEntity.kt | 30 ++ .../ConversationDetailsWithEventsMapper.kt | 214 ++++++++++ .../dao/conversation/ConversationMapper.kt | 2 +- .../persistence/dao/message/MessageDAOImpl.kt | 2 +- .../dao/message/draft/MessageDraftDAOImpl.kt | 18 +- .../dao/message/draft/MessageDraftMapper.kt | 31 ++ .../persistence/dao/ConversationDAOTest.kt | 401 ++++++++++++++++-- .../persistence/utils/stubs/MessageStubs.kt | 9 + 26 files changed, 1190 insertions(+), 238 deletions(-) create mode 100644 persistence/src/commonMain/db_user/migrations/87.sqm create mode 100644 persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDetailsWithEventsEntity.kt create mode 100644 persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDetailsWithEventsMapper.kt create mode 100644 persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/draft/MessageDraftMapper.kt diff --git a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/Conversation.kt b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/Conversation.kt index d0f93e5af78..293cf4e6fcb 100644 --- a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/Conversation.kt +++ b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/Conversation.kt @@ -293,15 +293,11 @@ sealed class ConversationDetails(open val conversation: Conversation) { override val conversation: Conversation, val otherUser: OtherUser, val userType: UserType, - val unreadEventCount: UnreadEventCount, - val lastMessage: MessagePreview? ) : ConversationDetails(conversation) data class Group( override val conversation: Conversation, val hasOngoingCall: Boolean = false, - val unreadEventCount: UnreadEventCount, - val lastMessage: MessagePreview?, val isSelfUserMember: Boolean, val isSelfUserCreator: Boolean, val selfRole: Conversation.Member.Role? @@ -344,6 +340,13 @@ sealed class ConversationDetails(open val conversation: Conversation) { ) } +data class ConversationDetailsWithEvents( + val conversationDetails: ConversationDetails, + val unreadEventCount: UnreadEventCount = emptyMap(), + val lastMessage: MessagePreview? = null, + val hasNewActivitiesToShow: Boolean = false, +) + fun ConversationDetails.interactionAvailability(): InteractionAvailability { val availability = when (this) { is ConversationDetails.Connection -> InteractionAvailability.DISABLED diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationMapper.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationMapper.kt index 07975ff9a7f..5fdb2101090 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationMapper.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationMapper.kt @@ -27,7 +27,8 @@ import com.wire.kalium.logic.data.id.TeamId import com.wire.kalium.logic.data.id.toApi import com.wire.kalium.logic.data.id.toDao import com.wire.kalium.logic.data.id.toModel -import com.wire.kalium.logic.data.message.MessagePreview +import com.wire.kalium.logic.data.message.MessageMapper +import com.wire.kalium.logic.data.message.UnreadEventType import com.wire.kalium.logic.data.mls.MLSPublicKeys import com.wire.kalium.logic.data.user.AvailabilityStatusMapper import com.wire.kalium.logic.data.user.BotService @@ -45,12 +46,14 @@ import com.wire.kalium.network.api.authenticated.conversation.ReceiptMode import com.wire.kalium.network.api.authenticated.serverpublickey.MLSPublicKeysDTO import com.wire.kalium.network.api.model.ConversationAccessDTO import com.wire.kalium.network.api.model.ConversationAccessRoleDTO +import com.wire.kalium.persistence.dao.conversation.ConversationDetailsWithEventsEntity import com.wire.kalium.persistence.dao.conversation.ConversationEntity import com.wire.kalium.persistence.dao.conversation.ConversationEntity.GroupState import com.wire.kalium.persistence.dao.conversation.ConversationEntity.Protocol import com.wire.kalium.persistence.dao.conversation.ConversationEntity.ProtocolInfo import com.wire.kalium.persistence.dao.conversation.ConversationViewEntity import com.wire.kalium.persistence.dao.conversation.ProposalTimerEntity +import com.wire.kalium.persistence.dao.unread.UnreadEventTypeEntity import com.wire.kalium.persistence.util.requireField import com.wire.kalium.util.DateTimeUtil import com.wire.kalium.util.time.UNIX_FIRST_DATE @@ -64,12 +67,8 @@ interface ConversationMapper { fun fromApiModel(mlsPublicKeysDTO: MLSPublicKeysDTO?): MLSPublicKeys? fun fromDaoModel(daoModel: ConversationViewEntity): Conversation fun fromDaoModel(daoModel: ConversationEntity): Conversation - fun fromDaoModelToDetails( - daoModel: ConversationViewEntity, - lastMessage: MessagePreview?, - unreadEventCount: UnreadEventCount? - ): ConversationDetails - + fun fromDaoModelToDetails(daoModel: ConversationViewEntity): ConversationDetails + fun fromDaoModelToDetailsWithEvents(daoModel: ConversationDetailsWithEventsEntity): ConversationDetailsWithEvents fun fromDaoModel(daoModel: ProposalTimerEntity): ProposalTimer fun toDAOAccess(accessList: Set): List fun toDAOAccessRole(accessRoleList: Set): List @@ -105,7 +104,8 @@ internal class ConversationMapperImpl( private val domainUserTypeMapper: DomainUserTypeMapper, private val connectionStatusMapper: ConnectionStatusMapper, private val conversationRoleMapper: ConversationRoleMapper, - private val receiptModeMapper: ReceiptModeMapper = MapperProvider.receiptModeMapper() + private val messageMapper: MessageMapper, + private val receiptModeMapper: ReceiptModeMapper = MapperProvider.receiptModeMapper(), ) : ConversationMapper { override fun fromApiModelToDaoModel( @@ -232,11 +232,7 @@ internal class ConversationMapperImpl( } @Suppress("ComplexMethod", "LongMethod") - override fun fromDaoModelToDetails( - daoModel: ConversationViewEntity, - lastMessage: MessagePreview?, - unreadEventCount: UnreadEventCount? - ): ConversationDetails = + override fun fromDaoModelToDetails(daoModel: ConversationViewEntity): ConversationDetails = with(daoModel) { when (type) { ConversationEntity.Type.SELF -> { @@ -266,8 +262,6 @@ internal class ConversationMapperImpl( activeOneOnOneConversationId = userActiveOneOnOneConversationId?.toModel() ), userType = domainUserTypeMapper.fromUserTypeEntity(userType), - unreadEventCount = unreadEventCount ?: mapOf(), - lastMessage = lastMessage ) } @@ -275,8 +269,6 @@ internal class ConversationMapperImpl( ConversationDetails.Group( conversation = fromConversationViewToEntity(daoModel), hasOngoingCall = callStatus != null, // todo: we can do better! - unreadEventCount = unreadEventCount ?: mapOf(), - lastMessage = lastMessage, isSelfUserMember = isMember, isSelfUserCreator = isCreator == 1L, selfRole = selfRole?.let { conversationRoleMapper.fromDAO(it) } @@ -325,6 +317,26 @@ internal class ConversationMapperImpl( } } + override fun fromDaoModelToDetailsWithEvents(daoModel: ConversationDetailsWithEventsEntity): ConversationDetailsWithEvents = + ConversationDetailsWithEvents( + conversationDetails = fromDaoModelToDetails(daoModel.conversationViewEntity), + unreadEventCount = daoModel.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 + } + }, + lastMessage = when { + daoModel.conversationViewEntity.archived -> null // no last message in archived conversations + daoModel.messageDraft != null -> messageMapper.fromDraftToMessagePreview(daoModel.messageDraft!!) + daoModel.lastMessage != null -> messageMapper.fromEntityToMessagePreview(daoModel.lastMessage!!) + else -> null + }, + ) + override fun fromDaoModel(daoModel: ProposalTimerEntity): ProposalTimer = ProposalTimer(idMapper.fromGroupIDEntity(daoModel.groupID), daoModel.firingDate) diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepository.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepository.kt index bb703ddad54..50215cc9f03 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepository.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepository.kt @@ -39,7 +39,6 @@ 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.message.UnreadEventType import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.di.MapperProvider import com.wire.kalium.logic.functional.Either @@ -75,16 +74,11 @@ import com.wire.kalium.persistence.dao.conversation.ConversationEntity import com.wire.kalium.persistence.dao.conversation.ConversationMetaDataDAO import com.wire.kalium.persistence.dao.member.MemberDAO import com.wire.kalium.persistence.dao.message.MessageDAO -import com.wire.kalium.persistence.dao.message.draft.MessageDraftDAO -import com.wire.kalium.persistence.dao.unread.UnreadEventTypeEntity import com.wire.kalium.util.DelicateKaliumApi import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.firstOrNull -import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.datetime.Instant @@ -131,6 +125,11 @@ interface ConversationRepository { suspend fun getConversationList(): Either>> suspend fun observeConversationList(): Flow> suspend fun observeConversationListDetails(fromArchive: Boolean): Flow> + suspend fun observeConversationListDetailsWithEvents( + fromArchive: Boolean = false, + onlyInteractionsEnabled: Boolean = false, + newActivitiesOnTop: Boolean = false, + ): Flow> suspend fun getConversationIds( type: Conversation.Type, protocol: Conversation.Protocol, @@ -319,7 +318,6 @@ internal class ConversationDataSource internal constructor( private val clientDAO: ClientDAO, private val clientApi: ClientApi, private val conversationMetaDataDAO: ConversationMetaDataDAO, - private val messageDraftDAO: MessageDraftDAO, private val idMapper: IdMapper = MapperProvider.idMapper(), private val conversationMapper: ConversationMapper = MapperProvider.conversationMapper(selfUserId), private val memberMapper: MemberMapper = MapperProvider.memberMapper(), @@ -352,11 +350,10 @@ internal class ConversationDataSource internal constructor( override suspend fun observeConversationDetailsById(conversationID: ConversationId): Flow> = conversationDAO.observeConversationDetailsById(conversationID.toDao()) .wrapStorageRequest() - // TODO we don't need last message and unread count here, we should discuss to divide model for list and for details .map { eitherConversationView -> eitherConversationView.flatMap { try { - Either.Right(conversationMapper.fromDaoModelToDetails(it, null, mapOf())) + Either.Right(conversationMapper.fromDaoModelToDetails(it)) } catch (error: IllegalArgumentException) { kaliumLogger.e("require field in conversation Details", error) Either.Left(StorageFailure.DataNotFound) @@ -515,33 +512,22 @@ internal class ConversationDataSource internal constructor( } override suspend fun observeConversationListDetails(fromArchive: Boolean): Flow> = - combine( - conversationDAO.getAllConversationDetails(fromArchive), - if (fromArchive) flowOf(listOf()) else messageDAO.observeLastMessages(), - messageDAO.observeConversationsUnreadEvents(), - messageDraftDAO.observeMessageDrafts() - ) { conversationList, lastMessageList, unreadEvents, drafts -> - val lastMessageMap = lastMessageList.associateBy { it.conversationId } - val messageDraftMap = drafts.filter { it.text.isNotBlank() }.associateBy { it.conversationId } - - conversationList.map { conversation -> - conversationMapper.fromDaoModelToDetails( - conversation, - lastMessage = messageDraftMap[conversation.id]?.let { messageMapper.fromDraftToMessagePreview(it) } - ?: lastMessageMap[conversation.id]?.let { messageMapper.fromEntityToMessagePreview(it) }, - unreadEventCount = unreadEvents.firstOrNull { it.conversationId == conversation.id }?.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 - } - } - ) - } + conversationDAO.getAllConversationDetails(fromArchive).map { conversationViewEntityList -> + conversationViewEntityList.map { conversationViewEntity -> conversationMapper.fromDaoModelToDetails(conversationViewEntity) } } + override suspend fun observeConversationListDetailsWithEvents( + fromArchive: Boolean, + onlyInteractionsEnabled: Boolean, + newActivitiesOnTop: Boolean, + ): Flow> = + conversationDAO.getAllConversationDetailsWithEvents(fromArchive, onlyInteractionsEnabled, newActivitiesOnTop) + .map { conversationDetailsWithEventsViewEntityList -> + conversationDetailsWithEventsViewEntityList.map { conversationDetailsWithEventsViewEntity -> + conversationMapper.fromDaoModelToDetailsWithEvents(conversationDetailsWithEventsViewEntity) + } + } + override suspend fun fetchMlsOneToOneConversation(userId: UserId): Either = wrapApiRequest { conversationApi.fetchMlsOneToOneConversation(userId.toApi()) @@ -994,7 +980,7 @@ internal class ConversationDataSource internal constructor( override suspend fun getConversationDetailsByMLSGroupId(mlsGroupId: GroupID): Either = wrapStorageRequest { conversationDAO.getConversationByGroupID(mlsGroupId.value) } - .map { conversationMapper.fromDaoModelToDetails(it, null, mapOf()) } + .map { conversationMapper.fromDaoModelToDetails(it) } override suspend fun observeUnreadArchivedConversationsCount(): Flow = conversationDAO.observeUnreadArchivedConversationsCount() diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/di/MapperProvider.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/di/MapperProvider.kt index bd1b7939132..52e90b92103 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/di/MapperProvider.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/di/MapperProvider.kt @@ -128,7 +128,8 @@ internal object MapperProvider { AvailabilityStatusMapperImpl(), DomainUserTypeMapperImpl(), ConnectionStatusMapperImpl(), - ConversationRoleMapperImpl() + ConversationRoleMapperImpl(), + MessageMapperImpl(selfUserId), ) fun conversationRoleMapper(): ConversationRoleMapper = ConversationRoleMapperImpl() diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/UserSessionScope.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/UserSessionScope.kt index 0a1604a92e4..2f5c14521a7 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/UserSessionScope.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/UserSessionScope.kt @@ -711,7 +711,6 @@ class UserSessionScope internal constructor( userStorage.database.clientDAO, authenticatedNetworkContainer.clientApi, userStorage.database.conversationMetaDataDAO, - userStorage.database.messageDraftDAO ) private val conversationGroupRepository: ConversationGroupRepository diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/call/CallRepositoryTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/call/CallRepositoryTest.kt index 8ed14ad1929..1b0b223aa64 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/call/CallRepositoryTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/call/CallRepositoryTest.kt @@ -82,7 +82,6 @@ import kotlinx.coroutines.test.runTest import kotlinx.coroutines.yield import kotlinx.datetime.Clock import kotlin.test.Test -import kotlin.test.assertContains import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertTrue @@ -174,10 +173,8 @@ class CallRepositoryTest { ConversationDetails.Group( Arrangement.groupConversation, false, - lastMessage = null, isSelfUserMember = true, isSelfUserCreator = true, - unreadEventCount = emptyMap(), selfRole = Conversation.Member.Role.Member ) ) @@ -214,10 +211,8 @@ class CallRepositoryTest { Either.Right( ConversationDetails.Group( Arrangement.groupConversation, - lastMessage = null, isSelfUserMember = true, isSelfUserCreator = true, - unreadEventCount = emptyMap(), selfRole = Conversation.Member.Role.Member ) ) @@ -270,10 +265,8 @@ class CallRepositoryTest { Either.Right( ConversationDetails.Group( Arrangement.groupConversation, - lastMessage = null, isSelfUserMember = true, isSelfUserCreator = true, - unreadEventCount = emptyMap(), selfRole = Conversation.Member.Role.Member ) ) @@ -315,10 +308,8 @@ class CallRepositoryTest { Either.Right( ConversationDetails.Group( Arrangement.groupConversation, - lastMessage = null, isSelfUserMember = true, isSelfUserCreator = true, - unreadEventCount = emptyMap(), selfRole = Conversation.Member.Role.Member ) ) @@ -374,10 +365,8 @@ class CallRepositoryTest { Either.Right( ConversationDetails.Group( Arrangement.groupConversation, - lastMessage = null, isSelfUserMember = true, isSelfUserCreator = true, - unreadEventCount = emptyMap(), selfRole = Conversation.Member.Role.Member ) ) @@ -1819,8 +1808,6 @@ class CallRepositoryTest { conversation = oneOnOneConversation, otherUser = TestUser.OTHER, userType = UserType.INTERNAL, - lastMessage = null, - unreadEventCount = emptyMap() ) val mlsProtocolInfo = Conversation.ProtocolInfo.MLS( diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/ConversationMapperTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/ConversationMapperTest.kt index 4861d10b8c0..b086f0f4bfc 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/ConversationMapperTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/ConversationMapperTest.kt @@ -19,10 +19,17 @@ package com.wire.kalium.logic.data.conversation import com.wire.kalium.logic.data.connection.ConnectionStatusMapper +import com.wire.kalium.logic.data.conversation.ConversationRepositoryTest.Companion.MESSAGE_DRAFT_ENTITY +import com.wire.kalium.logic.data.conversation.ConversationRepositoryTest.Companion.MESSAGE_PREVIEW_ENTITY import com.wire.kalium.logic.data.id.IdMapper import com.wire.kalium.logic.data.id.TeamId +import com.wire.kalium.logic.data.message.Message +import com.wire.kalium.logic.data.message.MessageMapper +import com.wire.kalium.logic.data.message.MessagePreview +import com.wire.kalium.logic.data.message.MessagePreviewContent import com.wire.kalium.logic.data.user.AvailabilityStatusMapper import com.wire.kalium.logic.data.user.type.DomainUserTypeMapper +import com.wire.kalium.logic.framework.TestConversation import com.wire.kalium.logic.framework.TestUser import com.wire.kalium.network.api.authenticated.conversation.ConvProtocol import com.wire.kalium.network.api.authenticated.conversation.ConversationMemberDTO @@ -35,7 +42,11 @@ import com.wire.kalium.network.api.model.ConversationAccessRoleDTO import com.wire.kalium.network.api.model.ConversationId import com.wire.kalium.network.api.model.UserId import com.wire.kalium.persistence.dao.QualifiedIDEntity +import com.wire.kalium.persistence.dao.conversation.ConversationDetailsWithEventsEntity import com.wire.kalium.persistence.dao.conversation.ConversationEntity +import com.wire.kalium.persistence.dao.message.MessagePreviewEntity +import com.wire.kalium.persistence.dao.message.draft.MessageDraftEntity +import com.wire.kalium.persistence.dao.unread.ConversationUnreadEventEntity import io.mockative.Mock import io.mockative.any import io.mockative.every @@ -45,6 +56,7 @@ import io.mockative.verify import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertIs class ConversationMapperTest { @@ -69,6 +81,9 @@ class ConversationMapperTest { @Mock val conversationMemberMapper = mock(ConversationRoleMapper::class) + @Mock + val messageMapper = mock(MessageMapper::class) + private lateinit var conversationMapper: ConversationMapper @BeforeTest @@ -81,7 +96,8 @@ class ConversationMapperTest { userAvailabilityStatusMapper, domainUserTypeMapper, connectionStatusMapper, - conversationMemberMapper + conversationMemberMapper, + messageMapper, ) } @@ -338,6 +354,66 @@ class ConversationMapperTest { assertEquals(expected, result) } + private fun mockPreviewMessage(content: MessagePreviewContent) = MessagePreview( + id = MESSAGE_PREVIEW_ENTITY.id, + conversationId = TestConversation.CONVERSATION.id, + content = content, + visibility = Message.Visibility.VISIBLE, + isSelfMessage = false, + senderUserId = TestUser.OTHER.id, + ) + private fun testConversationLastMessage( + lastMessage: MessagePreviewEntity? = null, + messageDraft: MessageDraftEntity? = null, + archived: Boolean = false, + assertion: (MessagePreview?) -> Unit + ) { + every { + protocolInfoMapper.fromEntity(any()) + }.returns(Conversation.ProtocolInfo.Proteus) + every { + conversationStatusMapper.fromMutedStatusDaoModel(any()) + }.returns(MutedConversationStatus.AllAllowed) + every { + messageMapper.fromEntityToMessagePreview(any()) + }.returns(mockPreviewMessage(MessagePreviewContent.WithUser.Text("sender", "message"))) + every { + messageMapper.fromDraftToMessagePreview(any()) + }.returns(mockPreviewMessage(MessagePreviewContent.Draft("draft"))) + val conversation = ConversationDetailsWithEventsEntity( + conversationViewEntity = TestConversation.VIEW_ENTITY.copy(archived = archived), + lastMessage = lastMessage, + messageDraft = messageDraft, + unreadEvents = ConversationUnreadEventEntity(TestConversation.VIEW_ENTITY.id, mapOf()), + ) + assertion(conversationMapper.fromDaoModelToDetailsWithEvents(conversation).lastMessage) + } + + @Test + fun givenConversationWithDraftAndLastMessage_whenMappingFromDAODetailsWithEventsToModel_thenReturnProperLastMessage() = + testConversationLastMessage( + lastMessage = MESSAGE_PREVIEW_ENTITY.copy(conversationId = TestConversation.VIEW_ENTITY.id), + messageDraft = MESSAGE_DRAFT_ENTITY.copy(conversationId = TestConversation.VIEW_ENTITY.id), + ) { lastMessage -> assertIs(lastMessage?.content) } // draft is always newer than last message + + @Test + fun givenConversationWithLastMessage_whenMappingFromDAODetailsWithEventsToModel_thenReturnProperLastMessage() = + testConversationLastMessage( + lastMessage = MESSAGE_PREVIEW_ENTITY.copy(conversationId = TestConversation.VIEW_ENTITY.id), + ) { lastMessage -> assertIs(lastMessage?.content) } + + @Test + fun givenConversationWithNoLastMessageAndDraft_whenMappingFromDAODetailsWithEventsToModel_thenReturnProperLastMessage() = + testConversationLastMessage { lastMessage -> assertEquals(null, lastMessage) } + + @Test + fun givenArchivedConversationWithDraftAndLastMessage_whenMappingFromDAODetailsWithEventsToModel_thenReturnProperLastMessage() = + testConversationLastMessage( + lastMessage = MESSAGE_PREVIEW_ENTITY.copy(conversationId = TestConversation.VIEW_ENTITY.id), + messageDraft = MESSAGE_DRAFT_ENTITY.copy(conversationId = TestConversation.VIEW_ENTITY.id), + archived = true, + ) { lastMessage -> assertEquals(null, lastMessage) } // do not return last message if conversation is archived + private companion object { val ORIGINAL_CONVERSATION_ID = ConversationId("original", "oDomain") val SELF_USER_TEAM_ID = TeamId("teamID") diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepositoryTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepositoryTest.kt index 1e4a82d7c6e..b1d9918a4af 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepositoryTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepositoryTest.kt @@ -33,6 +33,7 @@ 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.MessagePreviewContent import com.wire.kalium.logic.data.message.UnreadEventType import com.wire.kalium.logic.data.user.OtherUser import com.wire.kalium.logic.data.user.SelfUser @@ -43,7 +44,6 @@ import com.wire.kalium.logic.framework.TestConversation import com.wire.kalium.logic.framework.TestTeam import com.wire.kalium.logic.framework.TestUser import com.wire.kalium.logic.functional.Either -import com.wire.kalium.logic.sync.receiver.conversation.RenamedConversationEventHandler import com.wire.kalium.logic.util.arrangement.dao.MemberDAOArrangement import com.wire.kalium.logic.util.arrangement.dao.MemberDAOArrangementImpl import com.wire.kalium.logic.util.shouldFail @@ -80,6 +80,7 @@ import com.wire.kalium.persistence.dao.client.ClientDAO import com.wire.kalium.persistence.dao.client.ClientTypeEntity import com.wire.kalium.persistence.dao.client.DeviceTypeEntity import com.wire.kalium.persistence.dao.conversation.ConversationDAO +import com.wire.kalium.persistence.dao.conversation.ConversationDetailsWithEventsEntity import com.wire.kalium.persistence.dao.conversation.ConversationEntity import com.wire.kalium.persistence.dao.conversation.ConversationMetaDataDAO import com.wire.kalium.persistence.dao.conversation.ConversationViewEntity @@ -87,7 +88,6 @@ import com.wire.kalium.persistence.dao.message.MessageDAO import com.wire.kalium.persistence.dao.message.MessageEntity import com.wire.kalium.persistence.dao.message.MessagePreviewEntity import com.wire.kalium.persistence.dao.message.MessagePreviewEntityContent -import com.wire.kalium.persistence.dao.message.draft.MessageDraftDAO import com.wire.kalium.persistence.dao.message.draft.MessageDraftEntity import com.wire.kalium.persistence.dao.unread.ConversationUnreadEventEntity import com.wire.kalium.persistence.dao.unread.UnreadEventTypeEntity @@ -698,22 +698,24 @@ class ConversationRepositoryTest { conversationIdEntity, mapOf(UnreadEventTypeEntity.MESSAGE to unreadMessagesCount) ) + val conversationDetailsWithEventsEntity = ConversationDetailsWithEventsEntity( + conversationViewEntity = conversationEntity, + lastMessage = messagePreviewEntity, + unreadEvents = conversationUnreadEventEntity, + ) val (_, conversationRepository) = Arrangement() - .withConversations(listOf(conversationEntity)) - .withLastMessages(listOf(messagePreviewEntity)) - .withConversationUnreadEvents(listOf(conversationUnreadEventEntity)) - .withMessageDrafts(listOf()) + .withConversationDetailsWithEvents(listOf(conversationDetailsWithEventsEntity)) .arrange() // when - conversationRepository.observeConversationListDetails(shouldFetchFromArchivedConversations).test { + conversationRepository.observeConversationListDetailsWithEvents(shouldFetchFromArchivedConversations).test { val result = awaitItem() - assertContains(result.map { it.conversation.id }, conversationId) - val conversation = result.first { it.conversation.id == conversationId } + assertContains(result.map { it.conversationDetails.conversation.id }, conversationId) + val conversation = result.first { it.conversationDetails.conversation.id == conversationId } - assertIs(conversation) + assertIs(conversation.conversationDetails) assertEquals(conversation.unreadEventCount[UnreadEventType.MESSAGE], unreadMessagesCount) assertEquals( MapperProvider.messageMapper(TestUser.SELF.id).fromEntityToMessagePreview(messagePreviewEntity), @@ -731,10 +733,12 @@ class ConversationRepositoryTest { val conversationIdEntity = ConversationIDEntity("some_value", "some_domain") val conversationId = QualifiedID("some_value", "some_domain") val shouldFetchFromArchivedConversations = true + val messagePreviewEntity = MESSAGE_PREVIEW_ENTITY.copy(conversationId = conversationIdEntity) val conversationEntity = TestConversation.VIEW_ENTITY.copy( id = conversationIdEntity, type = ConversationEntity.Type.GROUP, + archived = true, ) val unreadMessagesCount = 5 @@ -742,22 +746,24 @@ class ConversationRepositoryTest { conversationIdEntity, mapOf(UnreadEventTypeEntity.MESSAGE to unreadMessagesCount) ) + val conversationDetailsWithEventsEntity = ConversationDetailsWithEventsEntity( + conversationViewEntity = conversationEntity, + lastMessage = messagePreviewEntity, + unreadEvents = conversationUnreadEventEntity, + ) val (_, conversationRepository) = Arrangement() - .withConversations(listOf(conversationEntity)) - .withLastMessages(listOf(MESSAGE_PREVIEW_ENTITY.copy(conversationId = conversationIdEntity))) - .withMessageDrafts(listOf()) - .withConversationUnreadEvents(listOf(conversationUnreadEventEntity)) + .withConversationDetailsWithEvents(listOf(conversationDetailsWithEventsEntity)) .arrange() // when - conversationRepository.observeConversationListDetails(shouldFetchFromArchivedConversations).test { + conversationRepository.observeConversationListDetailsWithEvents(shouldFetchFromArchivedConversations).test { val result = awaitItem() - assertContains(result.map { it.conversation.id }, conversationId) - val conversation = result.first { it.conversation.id == conversationId } + assertContains(result.map { it.conversationDetails.conversation.id }, conversationId) + val conversation = result.first { it.conversationDetails.conversation.id == conversationId } - assertIs(conversation) + assertIs(conversation.conversationDetails) assertEquals(conversation.unreadEventCount[UnreadEventType.MESSAGE], unreadMessagesCount) assertEquals(null, conversation.lastMessage) @@ -768,20 +774,25 @@ class ConversationRepositoryTest { @Test fun givenAGroupConversationHasNotNewMessages_whenGettingConversationDetails_ThenReturnZeroUnreadMessageCount() = runTest { // given + val conversationIdEntity = ConversationIDEntity("some_value", "some_domain") + val conversationId = QualifiedID("some_value", "some_domain") + val shouldFetchFromArchivedConversations = false val conversationEntity = TestConversation.VIEW_ENTITY.copy( + id = conversationIdEntity, type = ConversationEntity.Type.GROUP, ) + val conversationDetailsWithEventsEntity = ConversationDetailsWithEventsEntity(conversationViewEntity = conversationEntity) val (_, conversationRepository) = Arrangement() - .withExpectedObservableConversationDetails(conversationEntity) + .withConversationDetailsWithEvents(listOf(conversationDetailsWithEventsEntity)) .arrange() // when - conversationRepository.observeConversationDetailsById(TestConversation.ID).test { + conversationRepository.observeConversationListDetailsWithEvents(shouldFetchFromArchivedConversations).test { // then - val conversationDetail = awaitItem() + val conversation = awaitItem().first { it.conversationDetails.conversation.id == conversationId } - assertIs>(conversationDetail) - assertTrue { conversationDetail.value.lastMessage == null } + assertIs(conversation.conversationDetails) + assertTrue { conversation.lastMessage == null } awaitComplete() } @@ -791,29 +802,34 @@ class ConversationRepositoryTest { fun givenAOneToOneConversationHasNotNewMessages_whenGettingConversationDetails_ThenReturnZeroUnreadMessageCount() = runTest { // given + val conversationIdEntity = ConversationIDEntity("some_value", "some_domain") + val conversationId = QualifiedID("some_value", "some_domain") + val shouldFetchFromArchivedConversations = false val conversationEntity = TestConversation.VIEW_ENTITY.copy( + id = conversationIdEntity, type = ConversationEntity.Type.ONE_ON_ONE, - otherUserId = QualifiedIDEntity("otherUser", "domain") + otherUserId = QualifiedIDEntity("otherUser", "domain"), ) + val conversationDetailsWithEventsEntity = ConversationDetailsWithEventsEntity(conversationViewEntity = conversationEntity) val (_, conversationRepository) = Arrangement() - .withExpectedObservableConversationDetails(conversationEntity) + .withConversationDetailsWithEvents(listOf(conversationDetailsWithEventsEntity)) .arrange() // when - conversationRepository.observeConversationDetailsById(TestConversation.ID).test { + conversationRepository.observeConversationListDetailsWithEvents(shouldFetchFromArchivedConversations).test { // then - val conversationDetail = awaitItem() + val conversation = awaitItem().first { it.conversationDetails.conversation.id == conversationId } - assertIs>(conversationDetail) - assertTrue { conversationDetail.value.lastMessage == null } + assertIs(conversation.conversationDetails) + assertTrue { conversation.lastMessage == null } awaitComplete() } } @Test - fun givenAGroupConversationHasNewMessages_whenObservingConversationListDetails_ThenCorrectlyGetUnreadMessageCount() = runTest { + fun givenAOneToOneConversationHasNewMessages_whenObservingConversationListDetails_ThenCorrectlyGetUnreadMessageCount() = runTest { // given val conversationIdEntity = ConversationIDEntity("some_value", "some_domain") val conversationId = QualifiedID("some_value", "some_domain") @@ -829,28 +845,53 @@ class ConversationRepositoryTest { conversationIdEntity, mapOf(UnreadEventTypeEntity.MESSAGE to unreadMessagesCount) ) + val conversationDetailsWithEventsEntity = ConversationDetailsWithEventsEntity( + conversationViewEntity = conversationEntity, + unreadEvents = conversationUnreadEventEntity, + ) val (_, conversationRepository) = Arrangement() - .withConversations(listOf(conversationEntity)) - .withLastMessages(listOf()) - .withMessageDrafts(listOf()) - .withConversationUnreadEvents(listOf(conversationUnreadEventEntity)) + .withConversationDetailsWithEvents(listOf(conversationDetailsWithEventsEntity)) .arrange() // when - conversationRepository.observeConversationListDetails(shouldFetchFromArchivedConversations).test { + conversationRepository.observeConversationListDetailsWithEvents(shouldFetchFromArchivedConversations).test { val result = awaitItem() - assertContains(result.map { it.conversation.id }, conversationId) - val conversation = result.first { it.conversation.id == conversationId } + assertContains(result.map { it.conversationDetails.conversation.id }, conversationId) + val conversation = result.first { it.conversationDetails.conversation.id == conversationId } - assertIs(conversation) + assertIs(conversation.conversationDetails) assertEquals(conversation.unreadEventCount[UnreadEventType.MESSAGE], unreadMessagesCount) awaitComplete() } } + @Test + fun givenAConversationHasLastMessageAndDraft_whenObservingConversationListDetails_ThenCorrectlyGetLastMessage() = runTest { + // given + val conversationIdEntity = ConversationIDEntity("some_value", "some_domain") + val conversationId = QualifiedID("some_value", "some_domain") + val conversationEntity = TestConversation.VIEW_ENTITY.copy(id = conversationIdEntity, type = ConversationEntity.Type.GROUP) + val conversationDetailsWithEventsEntity = ConversationDetailsWithEventsEntity( + conversationViewEntity = conversationEntity, + lastMessage = MESSAGE_PREVIEW_ENTITY.copy(conversationId = conversationIdEntity), + messageDraft = MESSAGE_DRAFT_ENTITY.copy(conversationId = conversationIdEntity), + unreadEvents = ConversationUnreadEventEntity(conversationIdEntity, mapOf()), + ) + val (_, conversationRepository) = Arrangement() + .withConversationDetailsWithEvents(listOf(conversationDetailsWithEventsEntity)) + .arrange() + // when + conversationRepository.observeConversationListDetailsWithEvents(false).test { + val result = awaitItem() + val conversation = result.first { it.conversationDetails.conversation.id == conversationId } + assertIs(conversation.lastMessage?.content) + awaitComplete() + } + } + @Test fun givenAConversationDaoFailed_whenUpdatingTheConversationReadDate_thenShouldNotSucceed() = runTest { // given @@ -1405,16 +1446,9 @@ class ConversationRepositoryTest { @Mock private val messageDAO = mock(MessageDAO::class) - @Mock - private val messageDraftDAO = mock(MessageDraftDAO::class) - @Mock val conversationMetaDataDAO: ConversationMetaDataDAO = mock(ConversationMetaDataDAO::class) - @Mock - val renamedConversationEventHandler = - mock(RenamedConversationEventHandler::class) - val conversationRepository = ConversationDataSource( TestUser.USER_ID, @@ -1427,7 +1461,6 @@ class ConversationRepositoryTest { clientDao, clientApi, conversationMetaDataDAO, - messageDraftDAO ) @@ -1488,40 +1521,22 @@ class ConversationRepositoryTest { }.returns(response) } - suspend fun withConversationUnreadEvents(unreadEvents: List) = apply { - coEvery { - messageDAO.observeConversationsUnreadEvents() - }.returns(flowOf(unreadEvents)) - } - suspend fun withUnreadArchivedConversationsCount(unreadCount: Long) = apply { coEvery { conversationDAO.observeUnreadArchivedConversationsCount() }.returns(flowOf(unreadCount)) } - suspend fun withUnreadMessageCounter(unreadCounter: Map) = apply { - coEvery { - messageDAO.observeUnreadMessageCounter() - }.returns(flowOf(unreadCounter)) - } - suspend fun withConversations(conversations: List) = apply { coEvery { conversationDAO.getAllConversationDetails(any()) }.returns(flowOf(conversations)) } - suspend fun withLastMessages(messages: List) = apply { + suspend fun withConversationDetailsWithEvents(conversations: List) = apply { coEvery { - messageDAO.observeLastMessages() - }.returns(flowOf(messages)) - } - - suspend fun withMessageDrafts(messageDrafts: List) = apply { - coEvery { - messageDraftDAO.observeMessageDrafts() - }.returns(flowOf(messageDrafts)) + conversationDAO.getAllConversationDetailsWithEvents(any(), any(), any()) + }.returns(flowOf(conversations)) } suspend fun withUpdateConversationReadDateException(exception: Throwable) = apply { @@ -1799,6 +1814,7 @@ class ConversationRepositoryTest { isSelfMessage = false, senderUserId = USER_ENTITY_ID ) + val MESSAGE_DRAFT_ENTITY = MessageDraftEntity(TestConversation.VIEW_ENTITY.id, "text", null, null, listOf()) private val TEST_QUALIFIED_ID_ENTITY = PersistenceQualifiedId("value", "domain") val OTHER_USER_ID = UserId("otherValue", "domain") diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/EndCallOnConversationChangeUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/EndCallOnConversationChangeUseCaseTest.kt index 45c45fabca6..6212866cc8e 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/EndCallOnConversationChangeUseCaseTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/EndCallOnConversationChangeUseCaseTest.kt @@ -248,8 +248,6 @@ class EndCallOnConversationChangeUseCaseTest { private val groupConversationDetail = ConversationDetails.Group( conversation = conversation, hasOngoingCall = true, - unreadEventCount = mapOf(), - lastMessage = null, isSelfUserMember = false, isSelfUserCreator = false, selfRole = null @@ -258,8 +256,6 @@ class EndCallOnConversationChangeUseCaseTest { private val oneOnOneConversationDetail = ConversationDetails.OneOne( conversation = conversation, otherUser = otherUser, - unreadEventCount = mapOf(), - lastMessage = null, userType = UserType.ADMIN ) } diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/ObserveConversationDetailsUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/ObserveConversationDetailsUseCaseTest.kt index 83c62e4f838..3bf203d73db 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/ObserveConversationDetailsUseCaseTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/ObserveConversationDetailsUseCaseTest.kt @@ -80,20 +80,16 @@ class ObserveConversationDetailsUseCaseTest { Either.Right( ConversationDetails.Group( conversation, - lastMessage = null, isSelfUserMember = true, isSelfUserCreator = true, - unreadEventCount = emptyMap(), selfRole = Conversation.Member.Role.Member ) ), Either.Right( ConversationDetails.Group( conversation.copy(name = "New Name"), - lastMessage = null, isSelfUserMember = true, isSelfUserCreator = true, - unreadEventCount = emptyMap(), selfRole = Conversation.Member.Role.Member ) ) diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/ObserveConversationListDetailsUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/ObserveConversationListDetailsUseCaseTest.kt index 9e6206c4605..e2f9044bf6c 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/ObserveConversationListDetailsUseCaseTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/ObserveConversationListDetailsUseCaseTest.kt @@ -64,10 +64,8 @@ class ObserveConversationListDetailsUseCaseTest { val groupConversationDetails = ConversationDetails.Group( groupConversation, - lastMessage = null, isSelfUserMember = true, isSelfUserCreator = true, - unreadEventCount = emptyMap(), selfRole = Conversation.Member.Role.Member ) @@ -101,19 +99,15 @@ class ObserveConversationListDetailsUseCaseTest { val groupConversationDetails1 = ConversationDetails.Group( groupConversation1, - lastMessage = null, isSelfUserMember = true, isSelfUserCreator = true, - unreadEventCount = emptyMap(), selfRole = Conversation.Member.Role.Member ) val groupConversationDetails2 = ConversationDetails.Group( groupConversation2, - lastMessage = null, isSelfUserMember = true, isSelfUserCreator = true, - unreadEventCount = emptyMap(), selfRole = Conversation.Member.Role.Member ) @@ -146,10 +140,8 @@ class ObserveConversationListDetailsUseCaseTest { val selfConversationDetails = ConversationDetails.Self(selfConversation) val groupConversationDetails = ConversationDetails.Group( conversation = groupConversation, - lastMessage = null, isSelfUserMember = true, isSelfUserCreator = true, - unreadEventCount = emptyMap(), selfRole = Conversation.Member.Role.Member ) @@ -182,10 +174,8 @@ class ObserveConversationListDetailsUseCaseTest { val groupConversationUpdates = listOf( ConversationDetails.Group( groupConversation, - lastMessage = null, isSelfUserMember = true, isSelfUserCreator = true, - unreadEventCount = emptyMap(), selfRole = Conversation.Member.Role.Member ) ) @@ -194,15 +184,11 @@ class ObserveConversationListDetailsUseCaseTest { oneOnOneConversation, TestUser.OTHER, UserType.INTERNAL, - lastMessage = null, - unreadEventCount = emptyMap() ) val secondOneOnOneDetails = ConversationDetails.OneOne( oneOnOneConversation, TestUser.OTHER.copy(name = "New User Name"), UserType.INTERNAL, - lastMessage = null, - unreadEventCount = emptyMap() ) val oneOnOneDetailsChannel = Channel(Channel.UNLIMITED) @@ -236,10 +222,8 @@ class ObserveConversationListDetailsUseCaseTest { val fetchArchivedConversations = false val groupConversationDetails = ConversationDetails.Group( groupConversation, - lastMessage = null, isSelfUserMember = true, isSelfUserCreator = true, - unreadEventCount = emptyMap(), selfRole = Conversation.Member.Role.Member ) @@ -273,10 +257,8 @@ class ObserveConversationListDetailsUseCaseTest { val fetchArchivedConversations = false val groupConversationDetails = ConversationDetails.Group( groupConversation, - lastMessage = null, isSelfUserMember = true, isSelfUserCreator = true, - unreadEventCount = emptyMap(), selfRole = Conversation.Member.Role.Member ) @@ -304,10 +286,8 @@ class ObserveConversationListDetailsUseCaseTest { val groupConversationDetails = ConversationDetails.Group( groupConversation, - lastMessage = null, isSelfUserMember = true, isSelfUserCreator = true, - unreadEventCount = emptyMap(), selfRole = Conversation.Member.Role.Member ) diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/framework/TestConversationDetails.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/framework/TestConversationDetails.kt index 8a6a2eb9fce..1372c6fc446 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/framework/TestConversationDetails.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/framework/TestConversationDetails.kt @@ -41,16 +41,12 @@ object TestConversationDetails { TestConversation.ONE_ON_ONE(), TestUser.OTHER, UserType.EXTERNAL, - lastMessage = null, - unreadEventCount = emptyMap() ) val CONVERSATION_GROUP = ConversationDetails.Group( conversation = TestConversation.GROUP(), - lastMessage = null, isSelfUserCreator = true, isSelfUserMember = true, - unreadEventCount = emptyMap(), selfRole = Conversation.Member.Role.Member ) diff --git a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Conversations.sq b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Conversations.sq index c089835afe0..dae660c9710 100644 --- a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Conversations.sq +++ b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Conversations.sq @@ -278,6 +278,120 @@ LEFT JOIN Connection ON Connection.qualified_conversation = Conversation.qualifi LEFT JOIN User AS connection_user ON Connection.qualified_to = connection_user.qualified_id LEFT JOIN Call ON Call.id IS (SELECT id FROM Call WHERE Call.conversation_id = Conversation.qualified_id AND Call.status IS 'STILL_ONGOING' ORDER BY created_at DESC LIMIT 1); +CREATE VIEW IF NOT EXISTS ConversationDetailsWithEvents AS +SELECT + ConversationDetails.*, + CASE + WHEN ConversationDetails.type = 'GROUP' THEN + CASE + WHEN ConversationDetails.selfRole IS NOT NULL THEN 1 + ELSE 0 + END + WHEN ConversationDetails.type = 'ONE_ON_ONE' THEN + CASE + WHEN userDefederated = 1 THEN 0 + WHEN userDeleted = 1 THEN 0 + WHEN connectionStatus = 'BLOCKED' THEN 0 + WHEN legal_hold_status = 'DEGRADED' THEN 0 + ELSE 1 + END + ELSE 0 + END AS interactionEnabled, + UnreadEventCountsGrouped.knocksCount AS unreadKnocksCount, + UnreadEventCountsGrouped.missedCallsCount AS unreadMissedCallsCount, + UnreadEventCountsGrouped.mentionsCount AS unreadMentionsCount, + UnreadEventCountsGrouped.repliesCount AS unreadRepliesCount, + UnreadEventCountsGrouped.messagesCount AS unreadMessagesCount, + CASE + WHEN ConversationDetails.callStatus = 'STILL_ONGOING' AND ConversationDetails.type = 'GROUP' THEN 1 -- if ongoing call in a group, move it to the top + WHEN ConversationDetails.mutedStatus = 'ALL_ALLOWED' THEN + CASE + WHEN knocksCount + missedCallsCount + mentionsCount + repliesCount + messagesCount > 0 THEN 1 -- if any unread events, move it to the top + WHEN ConversationDetails.type = 'CONNECTION_PENDING' AND ConversationDetails.connectionStatus = 'PENDING' THEN 1 -- if received connection request, move it to the top + ELSE 0 + END + WHEN ConversationDetails.mutedStatus = 'ONLY_MENTIONS_AND_REPLIES_ALLOWED' THEN + CASE + WHEN mentionsCount + repliesCount > 0 THEN 1 -- only if unread mentions or replies, move it to the top + WHEN ConversationDetails.type = 'CONNECTION_PENDING' AND ConversationDetails.connectionStatus = 'PENDING' THEN 1 -- if received connection request, move it to the top + ELSE 0 + END + ELSE 0 + END AS hasNewActivitiesToShow, + LastMessagePreview.id AS lastMessageId, + LastMessagePreview.contentType AS lastMessageContentType, + LastMessagePreview.date AS lastMessageDate, + LastMessagePreview.visibility AS lastMessageVisibility, + LastMessagePreview.senderUserId AS lastMessageSenderUserId, + LastMessagePreview.isEphemeral AS lastMessageIsEphemeral, + LastMessagePreview.senderName AS lastMessageSenderName, + LastMessagePreview.senderConnectionStatus AS lastMessageSenderConnectionStatus, + LastMessagePreview.senderIsDeleted AS lastMessageSenderIsDeleted, + LastMessagePreview.selfUserId AS lastMessageSelfUserId, + LastMessagePreview.isSelfMessage AS lastMessageIsSelfMessage, + LastMessagePreview.memberChangeList AS lastMessageMemberChangeList, + LastMessagePreview.memberChangeType AS lastMessageMemberChangeType, + LastMessagePreview.updateConversationName AS lastMessageUpdateConversationName, + LastMessagePreview.conversationName AS lastMessageConversationName, + LastMessagePreview.isMentioningSelfUser AS lastMessageIsMentioningSelfUser, + LastMessagePreview.isQuotingSelfUser AS lastMessageIsQuotingSelfUser, + LastMessagePreview.text AS lastMessageText, + LastMessagePreview.assetMimeType AS lastMessageAssetMimeType, + LastMessagePreview.isUnread AS lastMessageIsUnread, + LastMessagePreview.shouldNotify AS lastMessageShouldNotify, + LastMessagePreview.mutedStatus AS lastMessageMutedStatus, + LastMessagePreview.conversationType AS lastMessageConversationType, + MessageDraft.text AS messageDraftText, + MessageDraft.edit_message_id AS messageDraftEditMessageId, + MessageDraft.quoted_message_id AS messageDraftQuotedMessageId, + MessageDraft.mention_list AS messageDraftMentionList +FROM ConversationDetails +LEFT JOIN UnreadEventCountsGrouped + ON ConversationDetails.qualifiedId = UnreadEventCountsGrouped.conversationId +LEFT JOIN LastMessagePreview ON ConversationDetails.qualifiedId = LastMessagePreview.conversationId AND ConversationDetails.archived = 0 -- only return last message for non-archived conversations +LEFT JOIN MessageDraft ON ConversationDetails.qualifiedId = MessageDraft.conversation_id AND ConversationDetails.archived = 0 -- only return message draft for non-archived conversations +WHERE + type IS NOT 'SELF' + AND ( + type IS 'GROUP' + OR (type IS 'ONE_ON_ONE' AND (name IS NOT NULL AND otherUserId IS NOT NULL)) -- show 1:1 convos if they have user metadata + OR (type IS 'ONE_ON_ONE' AND userDeleted = 1) -- show deleted 1:1 convos to maintain prev, logic + OR (type IS 'CONNECTION_PENDING' AND otherUserId IS NOT NULL) -- show connection requests even without metadata + ) + AND (protocol IS 'PROTEUS' OR protocol IS 'MIXED' OR (protocol IS 'MLS' AND mls_group_state IS 'ESTABLISHED')) + AND isActive; + +selectAllConversationDetailsWithEvents: +SELECT * FROM ConversationDetailsWithEvents +WHERE archived = :fromArchive + AND CASE WHEN :onlyInteractionsEnabled THEN interactionEnabled = 1 ELSE 1 END +ORDER BY + CASE WHEN :newActivitiesOnTop THEN hasNewActivitiesToShow ELSE 0 END DESC, + lastModifiedDate DESC, + name IS NULL, + name COLLATE NOCASE ASC; + +selectConversationDetailsWithEventsFromSearch: +SELECT * FROM ConversationDetailsWithEvents +WHERE + archived = :fromArchive + AND CASE WHEN :onlyInteractionsEnabled THEN interactionEnabled = 1 ELSE 1 END + AND name LIKE ('%' || :searchQuery || '%') +ORDER BY + CASE WHEN :newActivitiesOnTop THEN hasNewActivitiesToShow ELSE 0 END DESC, + lastModifiedDate DESC, + name IS NULL, + name COLLATE NOCASE ASC +LIMIT :limit +OFFSET :offset; + +countConversationDetailsWithEventsFromSearch: +SELECT COUNT(*) FROM ConversationDetailsWithEvents +WHERE + archived = :fromArchive + AND CASE WHEN :onlyInteractionsEnabled THEN interactionEnabled = 1 ELSE 1 END + AND name LIKE ('%' || :searchQuery || '%'); + selectAllConversationDetails: SELECT * FROM ConversationDetails WHERE diff --git a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/MessagePreview.sq b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/MessagePreview.sq index f57c6006a50..68c2bff623e 100644 --- a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/MessagePreview.sq +++ b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/MessagePreview.sq @@ -45,13 +45,41 @@ LEFT JOIN MessageConversationChangedContent AS ConversationNameChangedContent ON LEFT JOIN MessageAssetContent AS AssetContent ON Message.id = AssetContent.message_id AND Message.conversation_id = AssetContent.conversation_id LEFT JOIN MessageTextContent AS TextContent ON Message.id = TextContent.message_id AND Message.conversation_id = TextContent.conversation_id; -getLastMessages: -SELECT * FROM MessagePreview AS message -WHERE id IN ( - SELECT id FROM Message - WHERE - Message.visibility IN ('VISIBLE', 'DELETED') AND - Message.content_type IN ('TEXT', 'ASSET', 'KNOCK', 'MISSED_CALL', 'CONVERSATION_RENAMED', 'MEMBER_CHANGE', 'COMPOSITE', 'CONVERSATION_DEGRADED_MLS', 'CONVERSATION_DEGRADED_PROTEUS', 'CONVERSATION_VERIFIED_MLS', 'CONVERSATION_VERIFIED_PROTEUS', 'LOCATION') - GROUP BY Message.conversation_id - HAVING Message.creation_date = MAX(Message.creation_date) +CREATE VIEW IF NOT EXISTS LastMessagePreview +AS SELECT + MessagePreview.id AS id, + MessagePreview.conversationId AS conversationId, + MessagePreview.contentType AS contentType, + MessagePreview.date AS date, + MessagePreview.visibility AS visibility, + MessagePreview.senderUserId AS senderUserId, + MessagePreview.isEphemeral AS isEphemeral, + MessagePreview.senderName AS senderName, + MessagePreview.senderConnectionStatus AS senderConnectionStatus, + MessagePreview.senderIsDeleted AS senderIsDeleted, + MessagePreview.selfUserId AS selfUserId, + MessagePreview.isSelfMessage AS isSelfMessage, + MessagePreview.memberChangeList AS memberChangeList, + MessagePreview.memberChangeType AS memberChangeType, + MessagePreview.updateConversationName AS updateConversationName, + MessagePreview.conversationName AS conversationName, + MessagePreview.isMentioningSelfUser AS isMentioningSelfUser, + MessagePreview.isQuotingSelfUser AS isQuotingSelfUser, + MessagePreview.text AS text, + MessagePreview.assetMimeType AS assetMimeType, + MessagePreview.isUnread AS isUnread, + MessagePreview.shouldNotify AS shouldNotify, + MessagePreview.mutedStatus AS mutedStatus, + MessagePreview.conversationType AS conversationType +FROM MessagePreview +WHERE MessagePreview.id IN ( + SELECT id FROM Message + WHERE + Message.visibility IN ('VISIBLE', 'DELETED') AND + Message.content_type IN ('TEXT', 'ASSET', 'KNOCK', 'MISSED_CALL', 'CONVERSATION_RENAMED', 'MEMBER_CHANGE', 'COMPOSITE', 'CONVERSATION_DEGRADED_MLS', 'CONVERSATION_DEGRADED_PROTEUS', 'CONVERSATION_VERIFIED_MLS', 'CONVERSATION_VERIFIED_PROTEUS', 'LOCATION') + GROUP BY Message.conversation_id + HAVING Message.creation_date = MAX(Message.creation_date) ); + +getLastMessages: +SELECT * FROM LastMessagePreview; diff --git a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/UnreadEvents.sq b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/UnreadEvents.sq index 020b4745972..6f685e20e3b 100644 --- a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/UnreadEvents.sq +++ b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/UnreadEvents.sq @@ -30,7 +30,15 @@ WHERE id = :id AND conversation_id = :conversation_id; getUnreadEvents: SELECT conversation_id, type FROM UnreadEvent; -getConversationsUnreadEvents: +getConversationUnreadEventsCount: +SELECT COUNT(*) FROM UnreadEvent WHERE conversation_id = ?; + +getUnreadArchivedConversationsCount: +SELECT COUNT(DISTINCT ue.conversation_id) FROM UnreadEvent ue +INNER JOIN Conversation c ON ue.conversation_id = c.qualified_id +WHERE c.archived = 1; + +CREATE VIEW IF NOT EXISTS UnreadEventCountsGrouped AS SELECT conversation_id AS conversationId, SUM(CASE WHEN type = 'KNOCK' THEN 1 ELSE 0 END) AS knocksCount, @@ -41,13 +49,5 @@ SELECT FROM UnreadEvent GROUP BY conversation_id; -getPaginatedUnreadEvents: -SELECT * FROM UnreadEvent LIMIT ? OFFSET ?; - -getConversationUnreadEventsCount: -SELECT COUNT(*) FROM UnreadEvent WHERE conversation_id = ?; - -getUnreadArchivedConversationsCount: -SELECT COUNT(DISTINCT ue.conversation_id) FROM UnreadEvent ue -INNER JOIN Conversation c ON ue.conversation_id = c.qualified_id -WHERE c.archived = 1; +getConversationsUnreadEventCountsGrouped: +SELECT * FROM UnreadEventCountsGrouped; diff --git a/persistence/src/commonMain/db_user/migrations/87.sqm b/persistence/src/commonMain/db_user/migrations/87.sqm new file mode 100644 index 00000000000..60285644325 --- /dev/null +++ b/persistence/src/commonMain/db_user/migrations/87.sqm @@ -0,0 +1,129 @@ +CREATE VIEW IF NOT EXISTS LastMessagePreview +AS SELECT + MessagePreview.id AS id, + MessagePreview.conversationId AS conversationId, + MessagePreview.contentType AS contentType, + MessagePreview.date AS date, + MessagePreview.visibility AS visibility, + MessagePreview.senderUserId AS senderUserId, + MessagePreview.isEphemeral AS isEphemeral, + MessagePreview.senderName AS senderName, + MessagePreview.senderConnectionStatus AS senderConnectionStatus, + MessagePreview.senderIsDeleted AS senderIsDeleted, + MessagePreview.selfUserId AS selfUserId, + MessagePreview.isSelfMessage AS isSelfMessage, + MessagePreview.memberChangeList AS memberChangeList, + MessagePreview.memberChangeType AS memberChangeType, + MessagePreview.updateConversationName AS updateConversationName, + MessagePreview.conversationName AS conversationName, + MessagePreview.isMentioningSelfUser AS isMentioningSelfUser, + MessagePreview.isQuotingSelfUser AS isQuotingSelfUser, + MessagePreview.text AS text, + MessagePreview.assetMimeType AS assetMimeType, + MessagePreview.isUnread AS isUnread, + MessagePreview.shouldNotify AS shouldNotify, + MessagePreview.mutedStatus AS mutedStatus, + MessagePreview.conversationType AS conversationType +FROM MessagePreview +WHERE MessagePreview.id IN ( + SELECT id FROM Message + WHERE + Message.visibility IN ('VISIBLE', 'DELETED') AND + Message.content_type IN ('TEXT', 'ASSET', 'KNOCK', 'MISSED_CALL', 'CONVERSATION_RENAMED', 'MEMBER_CHANGE', 'COMPOSITE', 'CONVERSATION_DEGRADED_MLS', 'CONVERSATION_DEGRADED_PROTEUS', 'CONVERSATION_VERIFIED_MLS', 'CONVERSATION_VERIFIED_PROTEUS', 'LOCATION') + GROUP BY Message.conversation_id + HAVING Message.creation_date = MAX(Message.creation_date) +); + +CREATE VIEW IF NOT EXISTS UnreadEventCountsGrouped AS +SELECT + conversation_id AS conversationId, + SUM(CASE WHEN type = 'KNOCK' THEN 1 ELSE 0 END) AS knocksCount, + SUM(CASE WHEN type = 'MISSED_CALL' THEN 1 ELSE 0 END) AS missedCallsCount, + SUM(CASE WHEN type = 'MENTION' THEN 1 ELSE 0 END) AS mentionsCount, + SUM(CASE WHEN type = 'REPLY' THEN 1 ELSE 0 END) AS repliesCount, + SUM(CASE WHEN type = 'MESSAGE' THEN 1 ELSE 0 END) AS messagesCount +FROM UnreadEvent +GROUP BY conversation_id; + +CREATE VIEW IF NOT EXISTS ConversationDetailsWithEvents AS +SELECT + ConversationDetails.*, + CASE + WHEN ConversationDetails.type = 'GROUP' THEN + CASE + WHEN ConversationDetails.selfRole IS NOT NULL THEN 1 + ELSE 0 + END + WHEN ConversationDetails.type = 'ONE_ON_ONE' THEN + CASE + WHEN userDefederated = 1 THEN 0 + WHEN userDeleted = 1 THEN 0 + WHEN connectionStatus = 'BLOCKED' THEN 0 + WHEN legal_hold_status = 'DEGRADED' THEN 0 + ELSE 1 + END + ELSE 0 + END AS interactionEnabled, + UnreadEventCountsGrouped.knocksCount AS unreadKnocksCount, + UnreadEventCountsGrouped.missedCallsCount AS unreadMissedCallsCount, + UnreadEventCountsGrouped.mentionsCount AS unreadMentionsCount, + UnreadEventCountsGrouped.repliesCount AS unreadRepliesCount, + UnreadEventCountsGrouped.messagesCount AS unreadMessagesCount, + CASE + WHEN ConversationDetails.callStatus = 'STILL_ONGOING' AND ConversationDetails.type = 'GROUP' THEN 1 -- if ongoing call in a group, move it to the top + WHEN ConversationDetails.mutedStatus = 'ALL_ALLOWED' THEN + CASE + WHEN knocksCount + missedCallsCount + mentionsCount + repliesCount + messagesCount > 0 THEN 1 -- if any unread events, move it to the top + WHEN ConversationDetails.type = 'CONNECTION_PENDING' AND ConversationDetails.connectionStatus = 'PENDING' THEN 1 -- if received connection request, move it to the top + ELSE 0 + END + WHEN ConversationDetails.mutedStatus = 'ONLY_MENTIONS_AND_REPLIES_ALLOWED' THEN + CASE + WHEN mentionsCount + repliesCount > 0 THEN 1 -- only if unread mentions or replies, move it to the top + WHEN ConversationDetails.type = 'CONNECTION_PENDING' AND ConversationDetails.connectionStatus = 'PENDING' THEN 1 -- if received connection request, move it to the top + ELSE 0 + END + ELSE 0 + END AS hasNewActivitiesToShow, + LastMessagePreview.id AS lastMessageId, + LastMessagePreview.contentType AS lastMessageContentType, + LastMessagePreview.date AS lastMessageDate, + LastMessagePreview.visibility AS lastMessageVisibility, + LastMessagePreview.senderUserId AS lastMessageSenderUserId, + LastMessagePreview.isEphemeral AS lastMessageIsEphemeral, + LastMessagePreview.senderName AS lastMessageSenderName, + LastMessagePreview.senderConnectionStatus AS lastMessageSenderConnectionStatus, + LastMessagePreview.senderIsDeleted AS lastMessageSenderIsDeleted, + LastMessagePreview.selfUserId AS lastMessageSelfUserId, + LastMessagePreview.isSelfMessage AS lastMessageIsSelfMessage, + LastMessagePreview.memberChangeList AS lastMessageMemberChangeList, + LastMessagePreview.memberChangeType AS lastMessageMemberChangeType, + LastMessagePreview.updateConversationName AS lastMessageUpdateConversationName, + LastMessagePreview.conversationName AS lastMessageConversationName, + LastMessagePreview.isMentioningSelfUser AS lastMessageIsMentioningSelfUser, + LastMessagePreview.isQuotingSelfUser AS lastMessageIsQuotingSelfUser, + LastMessagePreview.text AS lastMessageText, + LastMessagePreview.assetMimeType AS lastMessageAssetMimeType, + LastMessagePreview.isUnread AS lastMessageIsUnread, + LastMessagePreview.shouldNotify AS lastMessageShouldNotify, + LastMessagePreview.mutedStatus AS lastMessageMutedStatus, + LastMessagePreview.conversationType AS lastMessageConversationType, + MessageDraft.text AS messageDraftText, + MessageDraft.edit_message_id AS messageDraftEditMessageId, + MessageDraft.quoted_message_id AS messageDraftQuotedMessageId, + MessageDraft.mention_list AS messageDraftMentionList +FROM ConversationDetails +LEFT JOIN UnreadEventCountsGrouped + ON ConversationDetails.qualifiedId = UnreadEventCountsGrouped.conversationId +LEFT JOIN LastMessagePreview ON ConversationDetails.qualifiedId = LastMessagePreview.conversationId AND ConversationDetails.archived = 0 -- only return last message for non-archived conversations +LEFT JOIN MessageDraft ON ConversationDetails.qualifiedId = MessageDraft.conversation_id AND ConversationDetails.archived = 0 -- only return message draft for non-archived conversations +WHERE + type IS NOT 'SELF' + AND ( + type IS 'GROUP' + OR (type IS 'ONE_ON_ONE' AND (name IS NOT NULL AND otherUserId IS NOT NULL)) -- show 1:1 convos if they have user metadata + OR (type IS 'ONE_ON_ONE' AND userDeleted = 1) -- show deleted 1:1 convos to maintain prev, logic + OR (type IS 'CONNECTION_PENDING' AND otherUserId IS NOT NULL) -- show connection requests even without metadata + ) + AND (protocol IS 'PROTEUS' OR protocol IS 'MIXED' OR (protocol IS 'MLS' AND mls_group_state IS 'ESTABLISHED')) + AND isActive; diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDAO.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDAO.kt index 16a04553680..734524cb16d 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDAO.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDAO.kt @@ -57,6 +57,11 @@ interface ConversationDAO { suspend fun updateAllConversationsNotificationDate() suspend fun getAllConversations(): Flow> suspend fun getAllConversationDetails(fromArchive: Boolean): Flow> + suspend fun getAllConversationDetailsWithEvents( + fromArchive: Boolean = false, + onlyInteractionEnabled: Boolean = false, + newActivitiesOnTop: Boolean = false, + ): Flow> suspend fun getConversationIds( type: ConversationEntity.Type, protocol: ConversationEntity.Protocol, diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDAOImpl.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDAOImpl.kt index 0f20386e2e5..741e27498b0 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDAOImpl.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDAOImpl.kt @@ -58,6 +58,8 @@ internal class ConversationDAOImpl internal constructor( private val unreadEventsQueries: UnreadEventsQueries, private val coroutineContext: CoroutineContext, ) : ConversationDAO { + private val conversationMapper = ConversationMapper + private val conversationDetailsWithEventsMapper = ConversationDetailsWithEventsMapper // region Get/Observe by ID @@ -90,7 +92,6 @@ internal class ConversationDAOImpl internal constructor( // endregion - private val conversationMapper = ConversationMapper() override suspend fun getSelfConversationId(protocol: ConversationEntity.Protocol) = withContext(coroutineContext) { conversationQueries.selfConversationId(protocol).executeAsOneOrNull() } @@ -214,6 +215,18 @@ internal class ConversationDAOImpl internal constructor( .flowOn(coroutineContext) } + override suspend fun getAllConversationDetailsWithEvents( + fromArchive: Boolean, + onlyInteractionEnabled: Boolean, + newActivitiesOnTop: Boolean, + ): Flow> { + return conversationQueries.selectAllConversationDetailsWithEvents( + fromArchive, onlyInteractionEnabled, newActivitiesOnTop, conversationDetailsWithEventsMapper::fromViewToModel + ).asFlow() + .mapToList() + .flowOn(coroutineContext) + } + override suspend fun getConversationIds( type: ConversationEntity.Type, protocol: ConversationEntity.Protocol, diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDetailsWithEventsEntity.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDetailsWithEventsEntity.kt new file mode 100644 index 00000000000..9176a26d5df --- /dev/null +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDetailsWithEventsEntity.kt @@ -0,0 +1,30 @@ +/* + * 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.persistence.dao.conversation + +import com.wire.kalium.persistence.dao.message.MessagePreviewEntity +import com.wire.kalium.persistence.dao.message.draft.MessageDraftEntity +import com.wire.kalium.persistence.dao.unread.ConversationUnreadEventEntity + +data class ConversationDetailsWithEventsEntity( + val conversationViewEntity: ConversationViewEntity, + val lastMessage: MessagePreviewEntity? = null, + val messageDraft: MessageDraftEntity? = null, + val unreadEvents: ConversationUnreadEventEntity = ConversationUnreadEventEntity(conversationViewEntity.id, mapOf()), + val hasNewActivitiesToShow: Boolean = false, +) diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDetailsWithEventsMapper.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDetailsWithEventsMapper.kt new file mode 100644 index 00000000000..4f18ed7390b --- /dev/null +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDetailsWithEventsMapper.kt @@ -0,0 +1,214 @@ +/* + * 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.persistence.dao.conversation + +import com.wire.kalium.persistence.dao.BotIdEntity +import com.wire.kalium.persistence.dao.ConnectionEntity +import com.wire.kalium.persistence.dao.QualifiedIDEntity +import com.wire.kalium.persistence.dao.SupportedProtocolEntity +import com.wire.kalium.persistence.dao.UserAvailabilityStatusEntity +import com.wire.kalium.persistence.dao.UserTypeEntity +import com.wire.kalium.persistence.dao.call.CallEntity +import com.wire.kalium.persistence.dao.member.MemberEntity +import com.wire.kalium.persistence.dao.message.MessageEntity +import com.wire.kalium.persistence.dao.message.MessageMapper +import com.wire.kalium.persistence.dao.message.draft.MessageDraftMapper +import com.wire.kalium.persistence.dao.unread.UnreadEventMapper +import kotlinx.datetime.Instant + +data object ConversationDetailsWithEventsMapper { + @Suppress("LongParameterList", "FunctionParameterNaming") + fun fromViewToModel( + qualifiedId: QualifiedIDEntity, + name: String?, + type: ConversationEntity.Type, + callStatus: CallEntity.Status?, + previewAssetId: QualifiedIDEntity?, + mutedStatus: ConversationEntity.MutedStatus, + teamId: String?, + lastModifiedDate_: Instant?, + lastReadDate: Instant, + userAvailabilityStatus: UserAvailabilityStatusEntity?, + userType: UserTypeEntity?, + botService: BotIdEntity?, + userDeleted: Boolean?, + userDefederated: Boolean?, + userSupportedProtocols: Set?, + connectionStatus: ConnectionEntity.State?, + otherUserId: QualifiedIDEntity?, + otherUserActiveConversationId: QualifiedIDEntity?, + isCreator: Long, + isActive: Long, + accentId: Int?, + lastNotifiedMessageDate: Instant?, + selfRole: MemberEntity.Role?, + protocol: ConversationEntity.Protocol, + mlsCipherSuite: ConversationEntity.CipherSuite, + mlsEpoch: Long, + mlsGroupId: String?, + mlsLastKeyingMaterialUpdateDate: Instant, + mlsGroupState: ConversationEntity.GroupState, + accessList: List, + accessRoleList: List, + teamId_: String?, + mlsProposalTimer: String?, + mutedTime: Long, + creatorId: String, + lastModifiedDate: Instant, + receiptMode: ConversationEntity.ReceiptMode, + messageTimer: Long?, + userMessageTimer: Long?, + incompleteMetadata: Boolean, + archived: Boolean, + archivedDateTime: Instant?, + mlsVerificationStatus: ConversationEntity.VerificationStatus, + proteusVerificationStatus: ConversationEntity.VerificationStatus, + legalHoldStatus: ConversationEntity.LegalHoldStatus, + interactionEnabled: Long, + unreadKnocksCount: Long?, + unreadMissedCallsCount: Long?, + unreadMentionsCount: Long?, + unreadRepliesCount: Long?, + unreadMessagesCount: Long?, + hasNewActivitiesToShow: Long, + lastMessageId: String?, + lastMessageContentType: MessageEntity.ContentType?, + lastMessageDate: Instant?, + lastMessageVisibility: MessageEntity.Visibility?, + lastMessageSenderUserId: QualifiedIDEntity?, + lastMessageIsEphemeral: Boolean?, + lastMessageSenderName: String?, + lastMessageSenderConnectionStatus: ConnectionEntity.State?, + lastMessageSenderIsDeleted: Boolean?, + lastMessageSelfUserId: QualifiedIDEntity?, + lastMessageIsSelfMessage: Boolean?, + lastMessageMemberChangeList: List?, + lastMessageMemberChangeType: MessageEntity.MemberChangeType?, + lastMessageUpdateConversationName: String?, + lastMessageConversationName: String?, + lastMessageIsMentioningSelfUser: Boolean?, + lastMessageIsQuotingSelfUser: Boolean?, + lastMessageText: String?, + lastMessageAssetMimeType: String?, + lastMessageIsUnread: Boolean?, + lastMessageShouldNotify: Long?, + lastMessageMutedStatus: ConversationEntity.MutedStatus?, + lastMessageConversationType: ConversationEntity.Type?, + messageDraftText: String?, + messageDraftEditMessageId: String?, + messageDraftQuotedMessageId: String?, + messageDraftMentionList: List?, + ): ConversationDetailsWithEventsEntity = ConversationDetailsWithEventsEntity( + conversationViewEntity = ConversationMapper.fromViewToModel( + qualifiedId = qualifiedId, + name = name, + type = type, + callStatus = callStatus, + previewAssetId = previewAssetId, + mutedStatus = mutedStatus, + teamId = teamId, + lastModifiedDate_ = lastModifiedDate_, + lastReadDate = lastReadDate, + userAvailabilityStatus = userAvailabilityStatus, + userType = userType, + botService = botService, + userDeleted = userDeleted, + userDefederated = userDefederated, + userSupportedProtocols = userSupportedProtocols, + connectionStatus = connectionStatus, + otherUserId = otherUserId, + otherUserActiveConversationId = otherUserActiveConversationId, + isCreator = isCreator, + isActive = isActive, + accentId = accentId, + lastNotifiedMessageDate = lastNotifiedMessageDate, + selfRole = selfRole, + protocol = protocol, + mlsCipherSuite = mlsCipherSuite, + mlsEpoch = mlsEpoch, + mlsGroupId = mlsGroupId, + mlsLastKeyingMaterialUpdateDate = mlsLastKeyingMaterialUpdateDate, + mlsGroupState = mlsGroupState, + accessList = accessList, + accessRoleList = accessRoleList, + teamId_ = teamId_, + mlsProposalTimer = mlsProposalTimer, + mutedTime = mutedTime, + creatorId = creatorId, + lastModifiedDate = lastModifiedDate, + receiptMode = receiptMode, + messageTimer = messageTimer, + userMessageTimer = userMessageTimer, + incompleteMetadata = incompleteMetadata, + archived = archived, + archivedDateTime = archivedDateTime, + mlsVerificationStatus = mlsVerificationStatus, + proteusVerificationStatus = proteusVerificationStatus, + legalHoldStatus = legalHoldStatus, + ), + unreadEvents = UnreadEventMapper.toConversationUnreadEntity( + conversationId = qualifiedId, + knocksCount = unreadKnocksCount, + missedCallsCount = unreadMissedCallsCount, + mentionsCount = unreadMentionsCount, + repliesCount = unreadRepliesCount, + messagesCount = unreadMessagesCount, + ), + lastMessage = if (lastMessageId != null && lastMessageContentType != null && lastMessageDate != null + && lastMessageVisibility != null && lastMessageSenderUserId != null && lastMessageIsEphemeral != null + && lastMessageIsSelfMessage != null && lastMessageIsMentioningSelfUser != null && lastMessageIsUnread != null + && lastMessageShouldNotify != null) { + MessageMapper.toPreviewEntity( + id = lastMessageId, + conversationId = qualifiedId, + contentType = lastMessageContentType, + date = lastMessageDate, + visibility = lastMessageVisibility, + senderUserId = lastMessageSenderUserId, + isEphemeral = lastMessageIsEphemeral, + senderName = lastMessageSenderName, + senderConnectionStatus = lastMessageSenderConnectionStatus, + senderIsDeleted = lastMessageSenderIsDeleted, + selfUserId = lastMessageSelfUserId, + isSelfMessage = lastMessageIsSelfMessage, + memberChangeList = lastMessageMemberChangeList, + memberChangeType = lastMessageMemberChangeType, + updatedConversationName = lastMessageUpdateConversationName, + conversationName = lastMessageConversationName, + isMentioningSelfUser = lastMessageIsMentioningSelfUser, + isQuotingSelfUser = lastMessageIsQuotingSelfUser, + text = lastMessageText, + assetMimeType = lastMessageAssetMimeType, + isUnread = lastMessageIsUnread, + isNotified = lastMessageShouldNotify, + mutedStatus = lastMessageMutedStatus, + conversationType = lastMessageConversationType, + ) + } else null, + messageDraft = if (!messageDraftText.isNullOrBlank()) { + MessageDraftMapper.toDao( + conversationId = qualifiedId, + text = messageDraftText, + editMessageId = messageDraftEditMessageId, + quotedMessageId = messageDraftQuotedMessageId, + mentionList = messageDraftMentionList ?: emptyList(), + ) + } else null, + hasNewActivitiesToShow = hasNewActivitiesToShow > 0L, + ) +} diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationMapper.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationMapper.kt index 061e57f4f90..d18ce129d2a 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationMapper.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationMapper.kt @@ -27,7 +27,7 @@ import com.wire.kalium.persistence.dao.call.CallEntity import com.wire.kalium.persistence.dao.member.MemberEntity import kotlinx.datetime.Instant -internal class ConversationMapper { +data object ConversationMapper { @Suppress("LongParameterList", "UnusedParameter", "FunctionParameterNaming") fun fromViewToModel( qualifiedId: QualifiedIDEntity, diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageDAOImpl.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageDAOImpl.kt index 7df0c58a1ae..06d1f66ba3d 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageDAOImpl.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageDAOImpl.kt @@ -374,7 +374,7 @@ internal class MessageDAOImpl internal constructor( messagePreviewQueries.getLastMessages(mapper::toPreviewEntity).asFlow().flowOn(coroutineContext).mapToList() override suspend fun observeConversationsUnreadEvents(): Flow> { - return unreadEventsQueries.getConversationsUnreadEvents(unreadEventMapper::toConversationUnreadEntity) + return unreadEventsQueries.getConversationsUnreadEventCountsGrouped(unreadEventMapper::toConversationUnreadEntity) .asFlow().mapToList() } diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/draft/MessageDraftDAOImpl.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/draft/MessageDraftDAOImpl.kt index e8e79502456..ad22d6fa995 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/draft/MessageDraftDAOImpl.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/draft/MessageDraftDAOImpl.kt @@ -20,8 +20,7 @@ package com.wire.kalium.persistence.dao.message.draft import app.cash.sqldelight.coroutines.asFlow import com.wire.kalium.persistence.MessageDraftsQueries import com.wire.kalium.persistence.dao.ConversationIDEntity -import com.wire.kalium.persistence.dao.QualifiedIDEntity -import com.wire.kalium.persistence.dao.message.MessageEntity +import com.wire.kalium.persistence.dao.message.draft.MessageDraftMapper.toDao import com.wire.kalium.persistence.util.mapToList import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOn @@ -57,19 +56,4 @@ class MessageDraftDAOImpl internal constructor( .asFlow() .flowOn(coroutineContext) .mapToList() - - private fun toDao( - conversationId: QualifiedIDEntity, - text: String?, - editMessageId: String?, - quotedMessageId: String?, - mentionList: List - ): MessageDraftEntity = - MessageDraftEntity( - conversationId = conversationId, - text = text.orEmpty(), - editMessageId = editMessageId, - quotedMessageId = quotedMessageId, - selectedMentionList = mentionList - ) } diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/draft/MessageDraftMapper.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/draft/MessageDraftMapper.kt new file mode 100644 index 00000000000..40d1e622ce3 --- /dev/null +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/draft/MessageDraftMapper.kt @@ -0,0 +1,31 @@ +/* + * 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.persistence.dao.message.draft + +import com.wire.kalium.persistence.dao.QualifiedIDEntity +import com.wire.kalium.persistence.dao.message.MessageEntity + +data object MessageDraftMapper { + fun toDao( + conversationId: QualifiedIDEntity, + text: String?, + editMessageId: String?, + quotedMessageId: String?, + mentionList: List + ): MessageDraftEntity = MessageDraftEntity(conversationId, text.orEmpty(), editMessageId, quotedMessageId, mentionList) +} diff --git a/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/ConversationDAOTest.kt b/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/ConversationDAOTest.kt index 64a3a78cf68..d67ff2ef108 100644 --- a/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/ConversationDAOTest.kt +++ b/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/ConversationDAOTest.kt @@ -22,6 +22,8 @@ import app.cash.turbine.test import com.wire.kalium.persistence.BaseDatabaseTest import com.wire.kalium.persistence.dao.asset.AssetDAO import com.wire.kalium.persistence.dao.asset.AssetEntity +import com.wire.kalium.persistence.dao.call.CallDAO +import com.wire.kalium.persistence.dao.call.CallEntity import com.wire.kalium.persistence.dao.client.ClientDAO import com.wire.kalium.persistence.dao.client.InsertClientParam import com.wire.kalium.persistence.dao.conversation.ConversationDAO @@ -38,8 +40,11 @@ import com.wire.kalium.persistence.dao.member.MemberEntity import com.wire.kalium.persistence.dao.message.MessageDAO import com.wire.kalium.persistence.dao.message.MessageEntity import com.wire.kalium.persistence.dao.message.MessageEntityContent +import com.wire.kalium.persistence.dao.message.draft.MessageDraftDAO +import com.wire.kalium.persistence.dao.unread.UnreadEventTypeEntity import com.wire.kalium.persistence.utils.IgnoreIOS import com.wire.kalium.persistence.utils.stubs.newConversationEntity +import com.wire.kalium.persistence.utils.stubs.newDraftMessageEntity import com.wire.kalium.persistence.utils.stubs.newRegularMessageEntity import com.wire.kalium.persistence.utils.stubs.newSystemMessageEntity import com.wire.kalium.persistence.utils.stubs.newUserEntity @@ -69,10 +74,12 @@ class ConversationDAOTest : BaseDatabaseTest() { private lateinit var clientDao: ClientDAO private lateinit var connectionDAO: ConnectionDAO private lateinit var messageDAO: MessageDAO + private lateinit var messageDraftDAO: MessageDraftDAO private lateinit var userDAO: UserDAO private lateinit var teamDAO: TeamDAO private lateinit var memberDAO: MemberDAO private lateinit var assertDAO: AssetDAO + private lateinit var callDAO: CallDAO private val selfUserId = UserIDEntity("selfValue", "selfDomain") @BeforeTest @@ -83,14 +90,16 @@ class ConversationDAOTest : BaseDatabaseTest() { clientDao = db.clientDAO connectionDAO = db.connectionDAO messageDAO = db.messageDAO + messageDraftDAO = db.messageDraftDAO userDAO = db.userDAO teamDAO = db.teamDAO memberDAO = db.memberDAO assertDAO = db.assetDAO + callDAO = db.callDAO } @Test - fun givenConversationIsInserted_whenFetchingById_thenConversationIsReturned() = runTest { + fun givenConversationIsInserted_whenFetchingById_thenConversationIsReturned() = runTest(dispatcher) { conversationDAO.insertConversation(conversationEntity1) insertTeamUserAndMember(team, user1, conversationEntity1.id) val result = conversationDAO.getConversationDetailsById(conversationEntity1.id) @@ -98,7 +107,7 @@ class ConversationDAOTest : BaseDatabaseTest() { } @Test - fun givenListOfConversations_ThenMultipleConversationsCanBeInsertedAtOnce() = runTest { + fun givenListOfConversations_ThenMultipleConversationsCanBeInsertedAtOnce() = runTest(dispatcher) { conversationDAO.insertConversations(listOf(conversationEntity1, conversationEntity2)) insertTeamUserAndMember(team, user1, conversationEntity1.id) insertTeamUserAndMember(team, user2, conversationEntity2.id) @@ -109,7 +118,7 @@ class ConversationDAOTest : BaseDatabaseTest() { } @Test - fun givenExistingConversation_ThenConversationCanBeDeleted() = runTest { + fun givenExistingConversation_ThenConversationCanBeDeleted() = runTest(dispatcher) { conversationDAO.insertConversation(conversationEntity1) conversationDAO.deleteConversationByQualifiedID(conversationEntity1.id) val result = try { @@ -121,7 +130,7 @@ class ConversationDAOTest : BaseDatabaseTest() { } @Test - fun givenExistingConversation_WhenReinserting_ThenGroupStateIsUpdated() = runTest { + fun givenExistingConversation_WhenReinserting_ThenGroupStateIsUpdated() = runTest(dispatcher) { conversationDAO.insertConversation(conversationEntity2) conversationDAO.insertConversation(conversationEntity2.copy( protocolInfo = mlsProtocolInfo1.copy( @@ -133,7 +142,7 @@ class ConversationDAOTest : BaseDatabaseTest() { } @Test - fun givenExistingConversation_ThenConversationCanBeUpdated() = runTest { + fun givenExistingConversation_ThenConversationCanBeUpdated() = runTest(dispatcher) { conversationDAO.insertConversation(conversationEntity1) insertTeamUserAndMember(team, user1, conversationEntity1.id) val updatedConversation1Entity = conversationEntity1.copy(name = "Updated conversation1") @@ -266,7 +275,7 @@ class ConversationDAOTest : BaseDatabaseTest() { } @Test - fun givenExistingConversation_ThenConversationGroupStateCanBeUpdated() = runTest { + fun givenExistingConversation_ThenConversationGroupStateCanBeUpdated() = runTest(dispatcher) { conversationDAO.insertConversation(conversationEntity2) conversationDAO.updateConversationGroupState( ConversationEntity.GroupState.PENDING_WELCOME_MESSAGE, @@ -279,7 +288,7 @@ class ConversationDAOTest : BaseDatabaseTest() { } @Test - fun givenExistingConversation_ThenConversationGroupStateCanBeUpdatedToEstablished() = runTest { + fun givenExistingConversation_ThenConversationGroupStateCanBeUpdatedToEstablished() = runTest(dispatcher) { conversationDAO.insertConversation(conversationEntity2) conversationDAO.updateMlsGroupStateAndCipherSuite( ConversationEntity.GroupState.PENDING_WELCOME_MESSAGE, @@ -297,7 +306,7 @@ class ConversationDAOTest : BaseDatabaseTest() { } @Test - fun givenExistingConversation_ThenConversationIsUpdatedOnInsert() = runTest { + fun givenExistingConversation_ThenConversationIsUpdatedOnInsert() = runTest(dispatcher) { conversationDAO.insertConversation(conversationEntity1) insertTeamUserAndMember(team, user1, conversationEntity1.id) val updatedConversation1Entity = conversationEntity1.copy(name = "Updated conversation1") @@ -307,7 +316,7 @@ class ConversationDAOTest : BaseDatabaseTest() { } @Test - fun givenAnExistingConversation_WhenUpdatingTheMutingStatus_ThenConversationShouldBeUpdated() = runTest { + fun givenAnExistingConversation_WhenUpdatingTheMutingStatus_ThenConversationShouldBeUpdated() = runTest(dispatcher) { conversationDAO.insertConversation(conversationEntity2) conversationDAO.updateConversationMutedStatus( conversationId = conversationEntity2.id, @@ -322,7 +331,7 @@ class ConversationDAOTest : BaseDatabaseTest() { @Test @IgnoreIOS - fun givenConversation_whenInsertingStoredConversation_thenLastChangesTimeIsNotChanged() = runTest { + fun givenConversation_whenInsertingStoredConversation_thenLastChangesTimeIsNotChanged() = runTest(dispatcher) { val convStored = conversationEntity1.copy( lastNotificationDate = "2022-04-30T15:36:00.000Z".toInstant(), lastModifiedDate = "2022-03-30T15:36:00.000Z".toInstant(), @@ -347,7 +356,7 @@ class ConversationDAOTest : BaseDatabaseTest() { } @Test - fun givenConversation_whenUpdatingAccessInfo_thenItsUpdated() = runTest { + fun givenConversation_whenUpdatingAccessInfo_thenItsUpdated() = runTest(dispatcher) { val convStored = conversationEntity1.copy( accessRole = listOf(ConversationEntity.AccessRole.TEAM_MEMBER), access = listOf(ConversationEntity.Access.INVITE) ) @@ -370,7 +379,7 @@ class ConversationDAOTest : BaseDatabaseTest() { } @Test - fun givenExistingConversation_whenUpdatingTheConversationLastReadDate_ThenTheConversationHasTheDate() = runTest { + fun givenExistingConversation_whenUpdatingTheConversationLastReadDate_ThenTheConversationHasTheDate() = runTest(dispatcher) { // given val expectedLastReadDate = Instant.fromEpochMilliseconds(1648654560000) @@ -388,7 +397,7 @@ class ConversationDAOTest : BaseDatabaseTest() { @Test fun givenExistingConversation_whenUpdatingTheConversationSeenDate_thenEmitTheNewConversationStateWithTheUpdatedSeenDate() = - runTest { + runTest(dispatcher) { // given val expectedConversationSeenDate = Instant.fromEpochMilliseconds(1648654560000) @@ -452,7 +461,7 @@ class ConversationDAOTest : BaseDatabaseTest() { } @Test - fun givenNewValue_whenUpdatingProtocol_thenItsUpdatedAndReportedAsChanged() = runTest { + fun givenNewValue_whenUpdatingProtocol_thenItsUpdatedAndReportedAsChanged() = runTest(dispatcher) { val conversation = conversationEntity5 val groupId = "groupId" val updatedCipherSuite = ConversationEntity.CipherSuite.MLS_256_DHKEMP521_AES256GCM_SHA512_P521 @@ -799,7 +808,7 @@ class ConversationDAOTest : BaseDatabaseTest() { } @Test - fun givenAConversation_whenChangingTheName_itReturnsTheUpdatedName() = runTest { + fun givenAConversation_whenChangingTheName_itReturnsTheUpdatedName() = runTest(dispatcher) { // given conversationDAO.insertConversation(conversationEntity3) insertTeamUserAndMember(team, user1, conversationEntity3.id) @@ -833,7 +842,7 @@ class ConversationDAOTest : BaseDatabaseTest() { } @Test - fun givenAConversation_whenUpdatingReceiptMode_itReturnsTheUpdatedValue() = runTest { + fun givenAConversation_whenUpdatingReceiptMode_itReturnsTheUpdatedValue() = runTest(dispatcher) { // given conversationDAO.insertConversation(conversationEntity1.copy(receiptMode = ConversationEntity.ReceiptMode.ENABLED)) @@ -846,7 +855,7 @@ class ConversationDAOTest : BaseDatabaseTest() { } @Test - fun givenSelfUserIsNotMemberOfConversation_whenGettingConversationDetails_itReturnsCorrectDetails() = runTest { + fun givenSelfUserIsNotMemberOfConversation_whenGettingConversationDetails_itReturnsCorrectDetails() = runTest(dispatcher) { // given conversationDAO.insertConversation(conversationEntity3) teamDAO.insertTeam(team) @@ -861,7 +870,7 @@ class ConversationDAOTest : BaseDatabaseTest() { } @Test - fun givenConversation_whenUpdatingMessageTimer_itReturnsCorrectTimer() = runTest { + fun givenConversation_whenUpdatingMessageTimer_itReturnsCorrectTimer() = runTest(dispatcher) { // given conversationDAO.insertConversation(conversationEntity3) val messageTimer = 60000L @@ -875,7 +884,7 @@ class ConversationDAOTest : BaseDatabaseTest() { } @Test - fun givenSelfUserIsCreatorOfConversation_whenGettingConversationDetails_itReturnsCorrectDetails() = runTest { + fun givenSelfUserIsCreatorOfConversation_whenGettingConversationDetails_itReturnsCorrectDetails() = runTest(dispatcher) { // given conversationDAO.insertConversation(conversationEntity3.copy(creatorId = selfUserId.value)) teamDAO.insertTeam(team) @@ -996,6 +1005,13 @@ class ConversationDAOTest : BaseDatabaseTest() { assertEquals(conversationId, result.id) assertEquals(ConversationEntity.Type.CONNECTION_PENDING, result.type) } + conversationDAO.getAllConversationDetailsWithEvents(fromArchive).first().let { + assertEquals(1, it.size) + val result = it.first() + + assertEquals(conversationId, result.conversationViewEntity.id) + assertEquals(ConversationEntity.Type.CONNECTION_PENDING, result.conversationViewEntity.type) + } } @Test @@ -1020,6 +1036,9 @@ class ConversationDAOTest : BaseDatabaseTest() { conversationDAO.getAllConversationDetails(fromArchive).first().let { assertEquals(1, it.size) } + conversationDAO.getAllConversationDetailsWithEvents(fromArchive).first().let { + assertEquals(1, it.size) + } } @Test @@ -1038,6 +1057,10 @@ class ConversationDAOTest : BaseDatabaseTest() { assertEquals(1, it.size) assertEquals(conversationEntity1.id, it.first().id) } + conversationDAO.getAllConversationDetailsWithEvents(fromArchive).first().let { + assertEquals(1, it.size) + assertEquals(conversationEntity1.id, it.first().conversationViewEntity.id) + } } @Test @@ -1053,9 +1076,12 @@ class ConversationDAOTest : BaseDatabaseTest() { memberDAO.insertMember(member1, conversationEntity1.id) memberDAO.insertMember(member2, conversationEntity2.id) - val result = conversationDAO.getAllConversationDetails(fromArchive).first() - - assertEquals(2, result.size) + conversationDAO.getAllConversationDetails(fromArchive).first().let { + assertEquals(2, it.size) + } + conversationDAO.getAllConversationDetailsWithEvents(fromArchive).first().let { + assertEquals(2, it.size) + } } @Test @@ -1071,9 +1097,12 @@ class ConversationDAOTest : BaseDatabaseTest() { memberDAO.insertMember(member1, conversationEntity1.id) memberDAO.insertMember(member2, conversationEntity2.id) - val result = conversationDAO.getAllConversationDetails(fromArchive).first() - - assertEquals(2, result.size) + conversationDAO.getAllConversationDetails(fromArchive).first().let { + assertEquals(2, it.size) + } + conversationDAO.getAllConversationDetailsWithEvents(fromArchive).first().let { + assertEquals(2, it.size) + } } @Test @@ -1086,9 +1115,12 @@ class ConversationDAOTest : BaseDatabaseTest() { // when val result = conversationDAO.getAllConversationDetails(fromArchive).first() + val resultWithEvents = conversationDAO.getAllConversationDetailsWithEvents(fromArchive).first() // then - assertEquals(conversation.toViewEntity(user1).copy(accentId = 0), result.firstOrNull { it.id == conversation.id }) + val expected = conversation.toViewEntity(user1).copy(accentId = 0) + assertEquals(expected, result.firstOrNull { it.id == conversation.id }) + assertEquals(expected, resultWithEvents.firstOrNull { it.conversationViewEntity.id == conversation.id }?.conversationViewEntity) } @Test @@ -1101,9 +1133,11 @@ class ConversationDAOTest : BaseDatabaseTest() { // when val result = conversationDAO.getAllConversationDetails(fromArchive).first() + val resultWithEvents = conversationDAO.getAllConversationDetailsWithEvents(fromArchive).first() // then - assertNull(result.firstOrNull { it.id == conversation.id }) + assertEquals(null, result.firstOrNull { it.id == conversation.id }) + assertEquals(null, resultWithEvents.firstOrNull { it.conversationViewEntity.id == conversation.id }) } @Test @@ -1132,10 +1166,13 @@ class ConversationDAOTest : BaseDatabaseTest() { // when val result = conversationDAO.getAllConversationDetails(fromArchive).first() + val resultWithEvents = conversationDAO.getAllConversationDetailsWithEvents(fromArchive).first() // then assertEquals(conversation2.id, result[0].id) + assertEquals(conversation2.id, resultWithEvents[0].conversationViewEntity.id) assertEquals(conversation1.id, result[1].id) + assertEquals(conversation1.id, resultWithEvents[1].conversationViewEntity.id) } @Test @@ -1166,11 +1203,15 @@ class ConversationDAOTest : BaseDatabaseTest() { // when val result = conversationDAO.getAllConversationDetails(fromArchive).first() + val resultWithEvents = conversationDAO.getAllConversationDetailsWithEvents(fromArchive).first() // then assertTrue(result.size == 1) + assertTrue(resultWithEvents.size == 1) assertTrue(!result[0].archived) + assertTrue(!resultWithEvents[0].conversationViewEntity.archived) assertEquals(conversation2.id, result[0].id) + assertEquals(conversation2.id, resultWithEvents[0].conversationViewEntity.id) } @Test @@ -1241,6 +1282,312 @@ class ConversationDAOTest : BaseDatabaseTest() { assertEquals(1, it.size) assertEquals(conversationEntity1.id, it.first().id) } + conversationDAO.getAllConversationDetailsWithEvents(fromArchive = false).first().let { + assertEquals(1, it.size) + assertEquals(conversationEntity1.id, it.first().conversationViewEntity.id) + } + } + + @Test + fun givenConversationWithUnreadMessageAndDraft_whenGettingAllConversationsWithEvents_thenShouldReturnCorrectValues() = runTest { + val conversationEntity = conversationEntity1.copy(lastReadDate = Instant.fromEpochMilliseconds(0)) + conversationDAO.insertConversation(conversationEntity) + + userDAO.upsertUser(user1.copy(activeOneOnOneConversationId = conversationEntity.id)) + + memberDAO.insertMember(member1, conversationEntity.id) + memberDAO.insertMember(member2, conversationEntity.id) + + val messageEntity = newRegularMessageEntity( + id = "unread_message_id", + conversationId = conversationEntity.id, + senderUserId = user1.id, + date = conversationEntity.lastReadDate.plus(1.seconds) // message received after last read date so it should be unread + ) + messageDAO.insertOrIgnoreMessage(messageEntity) + + val draftMessageEntity = newDraftMessageEntity(conversationId = conversationEntity.id) + messageDraftDAO.upsertMessageDraft(draftMessageEntity) + + conversationDAO.getAllConversationDetailsWithEvents(fromArchive = false).first().let { + assertEquals(conversationEntity.id, it.first().conversationViewEntity.id) + assertEquals(1, it.first().unreadEvents.unreadEvents[UnreadEventTypeEntity.MESSAGE]) + assertEquals(draftMessageEntity.text, it.first().messageDraft?.text) + assertEquals(messageEntity.id, it.first().lastMessage?.id) + } + } + + @Test + fun givenConversationWithoutEvents_whenGettingAllConversationsWithEvents_thenShouldReturnCorrectValues() = runTest { + val conversationEntity = conversationEntity1.copy(lastReadDate = Instant.fromEpochMilliseconds(0)) + conversationDAO.insertConversation(conversationEntity) + + userDAO.upsertUser(user1.copy(activeOneOnOneConversationId = conversationEntity.id)) + + memberDAO.insertMember(member1, conversationEntity.id) + memberDAO.insertMember(member2, conversationEntity.id) + + conversationDAO.getAllConversationDetailsWithEvents(fromArchive = false).first().let { + assertEquals(conversationEntity.id, it.first().conversationViewEntity.id) + assertEquals(0, it.first().unreadEvents.unreadEvents.size) + assertEquals(null, it.first().messageDraft) + assertEquals(null, it.first().lastMessage) + } + } + + @Test + fun givenArchiveConversationWithUnreadMessageAndDraft_whenGettingAllConversationsWithEvents_thenShouldReturnCorrectValues() = runTest { + val conversationEntity = conversationEntity1.copy( + archived = true, + lastReadDate = Instant.fromEpochMilliseconds(0L) + ) + conversationDAO.insertConversation(conversationEntity) + + userDAO.upsertUser(user1.copy(activeOneOnOneConversationId = conversationEntity.id)) + + memberDAO.insertMember(member1, conversationEntity.id) + memberDAO.insertMember(member2, conversationEntity.id) + + val messageEntity = newRegularMessageEntity( + id = "unread_message_id", + conversationId = conversationEntity.id, + senderUserId = user1.id, + date = conversationEntity.lastReadDate.plus(1.seconds) // message received after last read date so it should be unread + ) + messageDAO.insertOrIgnoreMessage(messageEntity) + + val draftMessageEntity = newDraftMessageEntity(conversationId = conversationEntity.id) + messageDraftDAO.upsertMessageDraft(draftMessageEntity) + + conversationDAO.getAllConversationDetailsWithEvents(fromArchive = true).first().let { + assertEquals(conversationEntity.id, it.first().conversationViewEntity.id) + assertEquals(1, it.first().unreadEvents.unreadEvents[UnreadEventTypeEntity.MESSAGE]) + assertEquals(null, it.first().messageDraft) // do not return draft for archived conversation + assertEquals(null, it.first().lastMessage) // do not return last message for archived conversation + } + } + + @Test + fun givenConversationWithStillOngoingCall_whenGettingAllConversationsWithEventsAndNewActivitiesOnTop_thenReturnRightOrder() = runTest { + val conversationEntity1 = conversationEntity1.copy( + id = ConversationIDEntity("conversation1", "domain"), + type = ConversationEntity.Type.GROUP, + mutedStatus = ConversationEntity.MutedStatus.ALL_ALLOWED, + lastModifiedDate = Instant.fromEpochMilliseconds(2) // conversation 1 is more recent than conversation 2 + ) + val conversationEntity2 = conversationEntity1.copy( + id = ConversationIDEntity("conversation2", "domain"), + type = ConversationEntity.Type.GROUP, + mutedStatus = ConversationEntity.MutedStatus.ALL_ALLOWED, + lastModifiedDate = Instant.fromEpochMilliseconds(1) // conversation 2 is less recent than conversation 1 + ) + conversationDAO.insertConversation(conversationEntity1) + conversationDAO.insertConversation(conversationEntity2) + userDAO.upsertUser(user1) + val callEntity = CallEntity( + conversationId = conversationEntity1.id, + id = "call_id", + status = CallEntity.Status.STILL_ONGOING, + callerId = "callerId", + conversationType = ConversationEntity.Type.GROUP, + type = CallEntity.Type.CONFERENCE + ) + callDAO.insertCall(callEntity.copy(conversationId = conversationEntity2.id)) // but conversation 2 has ongoing call + conversationDAO.getAllConversationDetailsWithEvents(newActivitiesOnTop = true).first().let { + assertEquals(conversationEntity2.id, it[0].conversationViewEntity.id) // first is the one with ongoing call + assertEquals(conversationEntity1.id, it[1].conversationViewEntity.id) // second is the other one even if it is more recent + } + } + + @Test + fun givenConversationWithUnreadEvents_whenGettingAllConversationsWithEventsAndNewActivitiesOnTop_thenReturnRightOrder() = runTest { + val conversationEntity1 = conversationEntity1.copy( + id = ConversationIDEntity("conversation1", "domain"), + type = ConversationEntity.Type.GROUP, + lastModifiedDate = Instant.fromEpochMilliseconds(2), // conversation 1 is more recent than conversation 2 + lastReadDate = Instant.fromEpochMilliseconds(2) // but it's also already read + ) + val conversationEntity2 = conversationEntity1.copy( + id = ConversationIDEntity("conversation2", "domain"), + type = ConversationEntity.Type.GROUP, + lastModifiedDate = Instant.fromEpochMilliseconds(0), // conversation 2 is less recent than conversation 1 + lastReadDate = Instant.fromEpochMilliseconds(0) // but it's still unread + ) + conversationDAO.insertConversation(conversationEntity1) + conversationDAO.insertConversation(conversationEntity2) + userDAO.upsertUser(user1) + val messageEntity = newRegularMessageEntity( + id = "unread_message_id", + conversationId = conversationEntity2.id, + senderUserId = user1.id, + date = Instant.fromEpochMilliseconds(1) // message received after last read date so it should be unread + ) + messageDAO.insertOrIgnoreMessage(messageEntity) + conversationDAO.getAllConversationDetailsWithEvents(newActivitiesOnTop = true).first().let { + assertEquals(conversationEntity2.id, it[0].conversationViewEntity.id) // first is the one with unread event + assertEquals(conversationEntity1.id, it[1].conversationViewEntity.id) // second is the other one even if it is more recent + } + } + + @Test + fun givenConversationWithUnreadEventsButMuted_whenGettingAllConversationsWithEventsAndNewActivitiesOnTop_thenReturnRightOrder() = + runTest { + val conversationEntity1 = conversationEntity1.copy( + id = ConversationIDEntity("conversation1", "domain"), + type = ConversationEntity.Type.GROUP, + lastModifiedDate = Instant.fromEpochMilliseconds(2), // conversation 1 is more recent than conversation 2 + lastReadDate = Instant.fromEpochMilliseconds(0), + mutedStatus = ConversationEntity.MutedStatus.ONLY_MENTIONS_AND_REPLIES_ALLOWED, // but new messages are muted + ) + val conversationEntity2 = conversationEntity1.copy( + id = ConversationIDEntity("conversation2", "domain"), + type = ConversationEntity.Type.GROUP, + lastModifiedDate = Instant.fromEpochMilliseconds(1), // conversation 2 is less recent than conversation 1 + lastReadDate = Instant.fromEpochMilliseconds(0), + mutedStatus = ConversationEntity.MutedStatus.ALL_ALLOWED, // but new messages are not muted + ) + conversationDAO.insertConversation(conversationEntity1) + conversationDAO.insertConversation(conversationEntity2) + userDAO.upsertUser(user1) + val messageEntity1 = newRegularMessageEntity( + id = "unread_message_id_1", + conversationId = conversationEntity1.id, + senderUserId = user1.id, + date = Instant.fromEpochMilliseconds(1) // message received after last read date so it should be unread + ) + val messageEntity2 = newRegularMessageEntity( + id = "unread_message_id_2", + conversationId = conversationEntity2.id, + senderUserId = user1.id, + date = Instant.fromEpochMilliseconds(1) // message received after last read date so it should be unread + ) + messageDAO.insertOrIgnoreMessage(messageEntity1) + messageDAO.insertOrIgnoreMessage(messageEntity2) + conversationDAO.getAllConversationDetailsWithEvents(newActivitiesOnTop = true).first().let { + assertEquals(conversationEntity2.id, it[0].conversationViewEntity.id) // first is the one with not muted new message + assertEquals(conversationEntity1.id, it[1].conversationViewEntity.id) // second is the other one even if it is more recent + } + } + + @Test + fun givenConversationWithUnreadEvents_whenGettingAllConversationsWithEventsAndNotNewActivitiesOnTop_thenReturnRightOrder() = runTest { + val conversationEntity1 = conversationEntity1.copy( + id = ConversationIDEntity("conversation1", "domain"), + type = ConversationEntity.Type.GROUP, + lastModifiedDate = Instant.fromEpochMilliseconds(2), // conversation 1 is more recent than conversation 2 + lastReadDate = Instant.fromEpochMilliseconds(2) // but it's also already read + ) + val conversationEntity2 = conversationEntity1.copy( + id = ConversationIDEntity("conversation2", "domain"), + type = ConversationEntity.Type.GROUP, + lastModifiedDate = Instant.fromEpochMilliseconds(0), // conversation 2 is less recent than conversation 1 + lastReadDate = Instant.fromEpochMilliseconds(0) // but it's still unread + ) + conversationDAO.insertConversation(conversationEntity1) + conversationDAO.insertConversation(conversationEntity2) + userDAO.upsertUser(user1) + val messageEntity = newRegularMessageEntity( + id = "unread_message_id", + conversationId = conversationEntity2.id, + senderUserId = user1.id, + date = Instant.fromEpochMilliseconds(1) // message received after last read date so it should be unread + ) + messageDAO.insertOrIgnoreMessage(messageEntity) + conversationDAO.getAllConversationDetailsWithEvents(newActivitiesOnTop = false).first().let { + assertEquals(conversationEntity1.id, it[0].conversationViewEntity.id) // first is the more recent one even if it's already read + assertEquals(conversationEntity2.id, it[1].conversationViewEntity.id) // second is the other one even if it has unread events + } + } + + @Test + fun givenReceivedConnectionRequest_whenGettingAllConversationsWithEventsAndNewActivitiesOnTop_thenReturnRightOrder() = runTest { + val conversationEntity1 = conversationEntity1.copy( + id = ConversationIDEntity("conversation1", "domain"), + type = ConversationEntity.Type.GROUP, + lastModifiedDate = Instant.fromEpochMilliseconds(2), // conversation 1 is more recent than conversation 2 + ) + val conversationEntity2 = conversationEntity1.copy( + id = ConversationIDEntity("conversation2", "domain"), + type = ConversationEntity.Type.CONNECTION_PENDING, + lastModifiedDate = Instant.fromEpochMilliseconds(1), // conversation 1 is more recent than conversation 2 + ) + val userEntity = user1.copy(connectionStatus = ConnectionEntity.State.PENDING) + val connectionEntity = ConnectionEntity( + conversationId = conversationEntity2.id.value, + from = userEntity.id.value, + lastUpdateDate = Instant.fromEpochMilliseconds(1), + qualifiedConversationId = conversationEntity2.id, + qualifiedToId = userEntity.id, + status = ConnectionEntity.State.PENDING, + toId = userEntity.id.value, + ) + conversationDAO.insertConversation(conversationEntity1) + conversationDAO.insertConversation(conversationEntity2) + connectionDAO.insertConnection(connectionEntity) + userDAO.upsertUser(userEntity) + conversationDAO.getAllConversationDetailsWithEvents(newActivitiesOnTop = true).first().let { + assertEquals(conversationEntity2.id, it[0].conversationViewEntity.id) // first is the one with received connection request + assertEquals(conversationEntity1.id, it[1].conversationViewEntity.id) // second is the other one even if it is more recent + } + } + + @Test + fun givenConnectionRequest_whenGettingAllConversationsWithEventsAndOnlyEnabledInteractions_thenDoNotReturnIt() = runTest { + val conversationEntity1 = conversationEntity1.copy( + id = ConversationIDEntity("conversation1", "domain"), + type = ConversationEntity.Type.CONNECTION_PENDING, + lastModifiedDate = Instant.fromEpochMilliseconds(2), // conversation 1 is more recent than conversation 2 + ) + val userEntity = user1.copy(connectionStatus = ConnectionEntity.State.PENDING) + val connectionEntity = ConnectionEntity( + conversationId = conversationEntity1.id.value, + from = userEntity.id.value, + lastUpdateDate = Instant.fromEpochMilliseconds(1), + qualifiedConversationId = conversationEntity1.id, + qualifiedToId = userEntity.id, + status = ConnectionEntity.State.PENDING, + toId = userEntity.id.value, + ) + conversationDAO.insertConversation(conversationEntity1) + connectionDAO.insertConnection(connectionEntity) + userDAO.upsertUser(userEntity) + conversationDAO.getAllConversationDetailsWithEvents(onlyInteractionEnabled = true).first().let { + assertEquals(0, it.size) + } + } + + @Test + fun givenAGroupConvWhichSelfUserLeft_whenGettingAllConversationsWithEventsAndOnlyEnabledInteractions_thenDoNotReturnIt() = runTest { + val conversationEntity1 = conversationEntity1.copy( + id = ConversationIDEntity("conversation1", "domain"), + type = ConversationEntity.Type.GROUP, + ) + val conversationEntity2 = conversationEntity1.copy(id = ConversationIDEntity("conversation2", "domain")) + conversationDAO.insertConversation(conversationEntity1) + conversationDAO.insertConversation(conversationEntity2) + userDAO.upsertUser(user1) + memberDAO.insertMember(MemberEntity(selfUserId, MemberEntity.Role.Member), conversationEntity1.id) + conversationDAO.getAllConversationDetailsWithEvents(onlyInteractionEnabled = true).first().let { + assertEquals(1, it.size) // self user is a member of only conversation1 + assertEquals(conversationEntity1.id, it.first().conversationViewEntity.id) + } + } + + @Test + fun givenAOneOneConvWithDeletedUser_whenGettingAllConversationsWithEventsAndOnlyEnabledInteractions_thenDoNotReturnIt() = runTest { + val conversationEntity1 = conversationEntity1.copy( + id = ConversationIDEntity("conversation1", "domain"), + type = ConversationEntity.Type.ONE_ON_ONE, + ) + val conversationEntity2 = conversationEntity1.copy(id = ConversationIDEntity("conversation2", "domain")) + conversationDAO.insertConversation(conversationEntity1) + conversationDAO.insertConversation(conversationEntity2) + userDAO.upsertUser(user1.copy(deleted = true)) + memberDAO.insertMember(MemberEntity(selfUserId, MemberEntity.Role.Member), conversationEntity1.id) + memberDAO.insertMember(MemberEntity(user1.id, MemberEntity.Role.Member), conversationEntity1.id) + conversationDAO.getAllConversationDetailsWithEvents(onlyInteractionEnabled = true).first().let { + assertEquals(0, it.size) + } } @Test diff --git a/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/utils/stubs/MessageStubs.kt b/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/utils/stubs/MessageStubs.kt index 4116e4d6d43..5c2760274a7 100644 --- a/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/utils/stubs/MessageStubs.kt +++ b/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/utils/stubs/MessageStubs.kt @@ -23,6 +23,7 @@ import com.wire.kalium.persistence.dao.UserDetailsEntity import com.wire.kalium.persistence.dao.UserIDEntity import com.wire.kalium.persistence.dao.message.MessageEntity import com.wire.kalium.persistence.dao.message.MessageEntityContent +import com.wire.kalium.persistence.dao.message.draft.MessageDraftEntity import kotlinx.datetime.Instant @Suppress("LongParameterList") @@ -84,3 +85,11 @@ fun newSystemMessageEntity( selfDeletionEndDate = null, readCount = 0 ) + +fun newDraftMessageEntity( + conversationId: QualifiedIDEntity = QualifiedIDEntity("convId", "convDomain"), + text: String = "draft text", + editMessageId: String? = null, + quotedMessageId: String? = null, + selectedMentionList: List = emptyList() +) = MessageDraftEntity(conversationId, text, editMessageId, quotedMessageId, selectedMentionList) From 5a685045f37b3337914b1917c1f5ba5a590fe710 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Saleniuk?= Date: Wed, 9 Oct 2024 14:22:06 +0200 Subject: [PATCH 2/6] feat: conversation list pagination, pt2 - pagers [WPB-9433] --- .../ConversationScopeExtensions.kt | 21 ++ ...onDetailsWithEventsBySearchQueryUseCase.kt | 48 +++++ .../conversation/ConversationRepository.kt | 4 + .../ConversationRepositoryExtensions.kt | 84 ++++++++ .../feature/conversation/ConversationScope.kt | 6 +- .../ConversationRepositoryExtensionsTest.kt | 105 ++++++++++ .../dao/conversation/ConversationDAO.kt | 1 + .../dao/conversation/ConversationDAOImpl.kt | 2 + .../conversation/ConversationExtensions.kt | 74 +++++++ .../ConversationExtensionsTest.kt | 197 ++++++++++++++++++ 10 files changed, 540 insertions(+), 2 deletions(-) create mode 100644 logic/src/androidMain/kotlin/com/wire/kalium/logic/feature/conversation/ConversationScopeExtensions.kt create mode 100644 logic/src/androidMain/kotlin/com/wire/kalium/logic/feature/conversation/GetPaginatedFlowOfConversationDetailsWithEventsBySearchQueryUseCase.kt create mode 100644 logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepositoryExtensions.kt create mode 100644 logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepositoryExtensionsTest.kt create mode 100644 persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationExtensions.kt create mode 100644 persistence/src/commonTest/kotlin/com/wire/kalium/persistence/conversation/ConversationExtensionsTest.kt diff --git a/logic/src/androidMain/kotlin/com/wire/kalium/logic/feature/conversation/ConversationScopeExtensions.kt b/logic/src/androidMain/kotlin/com/wire/kalium/logic/feature/conversation/ConversationScopeExtensions.kt new file mode 100644 index 00000000000..86834de9f89 --- /dev/null +++ b/logic/src/androidMain/kotlin/com/wire/kalium/logic/feature/conversation/ConversationScopeExtensions.kt @@ -0,0 +1,21 @@ +/* + * 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 + +val ConversationScope.getPaginatedFlowOfConversationDetailsWithEventsBySearchQuery + get() = GetPaginatedFlowOfConversationDetailsWithEventsBySearchQueryUseCase(dispatcher, conversationRepository) diff --git a/logic/src/androidMain/kotlin/com/wire/kalium/logic/feature/conversation/GetPaginatedFlowOfConversationDetailsWithEventsBySearchQueryUseCase.kt b/logic/src/androidMain/kotlin/com/wire/kalium/logic/feature/conversation/GetPaginatedFlowOfConversationDetailsWithEventsBySearchQueryUseCase.kt new file mode 100644 index 00000000000..f01ce1ad0ed --- /dev/null +++ b/logic/src/androidMain/kotlin/com/wire/kalium/logic/feature/conversation/GetPaginatedFlowOfConversationDetailsWithEventsBySearchQueryUseCase.kt @@ -0,0 +1,48 @@ +/* + * 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 + +import androidx.paging.PagingConfig +import androidx.paging.PagingData +import com.wire.kalium.logic.data.conversation.ConversationDetailsWithEvents +import com.wire.kalium.logic.data.conversation.ConversationRepository +import com.wire.kalium.util.KaliumDispatcher +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn + +/** + * This use case will observe and return a flow of paginated searched conversation details with last message and unread events counts. + * @see PagingData + * @see ConversationDetailsWithEvents + */ +class GetPaginatedFlowOfConversationDetailsWithEventsBySearchQueryUseCase internal constructor( + private val dispatcher: KaliumDispatcher, + private val conversationRepository: ConversationRepository, +) { + suspend operator fun invoke( + searchQuery: String, + fromArchive: Boolean, + onlyInteractionsEnabled: Boolean, + newActivitiesOnTop: Boolean, + startingOffset: Long, + pagingConfig: PagingConfig + ): Flow> = conversationRepository.extensions + .getPaginatedConversationDetailsWithEventsBySearchQuery( + searchQuery, fromArchive, onlyInteractionsEnabled, newActivitiesOnTop, pagingConfig, startingOffset + ).flowOn(dispatcher.io) +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepository.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepository.kt index 50215cc9f03..1e86404efe9 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepository.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepository.kt @@ -84,6 +84,8 @@ import kotlinx.datetime.Instant @Suppress("TooManyFunctions") interface ConversationRepository { + val extensions: ConversationRepositoryExtensions + // region Get/Observe by id suspend fun observeConversationById(conversationId: ConversationId): Flow> @@ -327,6 +329,8 @@ internal class ConversationDataSource internal constructor( private val messageMapper: MessageMapper = MapperProvider.messageMapper(selfUserId), private val receiptModeMapper: ReceiptModeMapper = MapperProvider.receiptModeMapper() ) : ConversationRepository { + override val extensions: ConversationRepositoryExtensions = + ConversationRepositoryExtensionsImpl(conversationDAO, conversationMapper, messageMapper) // region Get/Observe by id diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepositoryExtensions.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepositoryExtensions.kt new file mode 100644 index 00000000000..6e7d70cc0d9 --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepositoryExtensions.kt @@ -0,0 +1,84 @@ +/* + * 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 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.message.KaliumPager +import com.wire.kalium.persistence.dao.unread.UnreadEventTypeEntity +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +interface ConversationRepositoryExtensions { + suspend fun getPaginatedConversationDetailsWithEventsBySearchQuery( + searchQuery: String, + fromArchive: Boolean, + onlyInteractionsEnabled: Boolean, + newActivitiesOnTop: Boolean, + pagingConfig: PagingConfig, + startingOffset: Long, + ): Flow> +} + +class ConversationRepositoryExtensionsImpl internal constructor( + private val conversationDAO: ConversationDAO, + private val conversationMapper: ConversationMapper, + private val messageMapper: MessageMapper, +) : ConversationRepositoryExtensions { + override suspend fun getPaginatedConversationDetailsWithEventsBySearchQuery( + searchQuery: String, + fromArchive: Boolean, + onlyInteractionsEnabled: Boolean, + newActivitiesOnTop: Boolean, + pagingConfig: PagingConfig, + startingOffset: Long + ): Flow> { + val pager: KaliumPager = + conversationDAO.platformExtensions.getPagerForConversationDetailsWithEventsSearch( + pagingConfig, searchQuery, fromArchive, onlyInteractionsEnabled, newActivitiesOnTop, startingOffset + ) + + 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, + ) + } + } + } +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ConversationScope.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ConversationScope.kt index bb3b7c29645..b471f6c742c 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ConversationScope.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ConversationScope.kt @@ -71,12 +71,13 @@ import com.wire.kalium.logic.feature.team.DeleteTeamConversationUseCaseImpl import com.wire.kalium.logic.sync.SyncManager import com.wire.kalium.logic.sync.receiver.conversation.RenamedConversationEventHandler import com.wire.kalium.logic.sync.receiver.handler.CodeUpdateHandlerImpl +import com.wire.kalium.util.KaliumDispatcher import com.wire.kalium.util.KaliumDispatcherImpl import kotlinx.coroutines.CoroutineScope @Suppress("LongParameterList") class ConversationScope internal constructor( - private val conversationRepository: ConversationRepository, + internal val conversationRepository: ConversationRepository, private val conversationGroupRepository: ConversationGroupRepository, private val connectionRepository: ConnectionRepository, private val userRepository: UserRepository, @@ -101,7 +102,8 @@ class ConversationScope internal constructor( private val scope: CoroutineScope, private val kaliumLogger: KaliumLogger, private val refreshUsersWithoutMetadata: RefreshUsersWithoutMetadataUseCase, - private val serverConfigLinks: ServerConfig.Links + private val serverConfigLinks: ServerConfig.Links, + internal val dispatcher: KaliumDispatcher = KaliumDispatcherImpl, ) { val getConversations: GetConversationsUseCase diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepositoryExtensionsTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepositoryExtensionsTest.kt new file mode 100644 index 00000000000..c04e8d736d8 --- /dev/null +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepositoryExtensionsTest.kt @@ -0,0 +1,105 @@ +/* + * 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 app.cash.paging.Pager +import app.cash.paging.PagingConfig +import app.cash.paging.PagingSource +import app.cash.paging.PagingState +import com.wire.kalium.logic.data.message.MessageMapper +import com.wire.kalium.logic.framework.TestConversationDetails +import com.wire.kalium.logic.framework.TestMessage +import com.wire.kalium.persistence.dao.conversation.ConversationDAO +import com.wire.kalium.persistence.dao.conversation.ConversationDetailsWithEventsEntity +import com.wire.kalium.persistence.dao.conversation.ConversationExtensions +import com.wire.kalium.persistence.dao.message.KaliumPager +import io.mockative.Mock +import io.mockative.any +import io.mockative.eq +import io.mockative.every +import io.mockative.mock +import io.mockative.once +import io.mockative.verify +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.runTest +import kotlin.test.Test + +class ConversationRepositoryExtensionsTest { + private val fakePagingSource = object : PagingSource() { + override fun getRefreshKey(state: PagingState): Int? = null + + override suspend fun load(params: LoadParams): LoadResult = + LoadResult.Error(NotImplementedError("STUB for tests. Not implemented.")) + } + + @Test + fun givenParameters_whenPaginatedConversationDetailsWithEvents_thenShouldCallDaoExtensionsWithRightParameters() = runTest { + val pagingConfig = PagingConfig(20) + val pager = Pager(pagingConfig) { fakePagingSource } + val kaliumPager = KaliumPager(pager, fakePagingSource, StandardTestDispatcher()) + val (arrangement, conversationRepositoryExtensions) = Arrangement() + .withConversationExtensionsReturningPager(kaliumPager) + .arrange() + val searchQuery = "search" + conversationRepositoryExtensions.getPaginatedConversationDetailsWithEventsBySearchQuery( + searchQuery = searchQuery, + fromArchive = false, + onlyInteractionsEnabled = false, + newActivitiesOnTop = false, + pagingConfig = pagingConfig, + startingOffset = 0L + ) + verify { + arrangement.conversationDaoExtensions + .getPagerForConversationDetailsWithEventsSearch(eq(pagingConfig), eq(searchQuery), eq(false), eq(false), eq(false), any()) + }.wasInvoked(exactly = once) + } + + private class Arrangement { + @Mock + val conversationDaoExtensions: ConversationExtensions = mock(ConversationExtensions::class) + @Mock + private val conversationDAO: ConversationDAO = mock(ConversationDAO::class) + @Mock + private val conversationMapper: ConversationMapper = mock(ConversationMapper::class) + @Mock + private val messageMapper: MessageMapper = mock(MessageMapper::class) + private val conversationRepositoryExtensions: ConversationRepositoryExtensions by lazy { + ConversationRepositoryExtensionsImpl(conversationDAO, conversationMapper, messageMapper) + } + + init { + every { + messageMapper.fromEntityToMessage(any()) + }.returns(TestMessage.TEXT_MESSAGE) + every { + conversationMapper.fromDaoModelToDetails(any()) + }.returns(TestConversationDetails.CONVERSATION_GROUP) + every { + conversationDAO.platformExtensions + }.returns(conversationDaoExtensions) + } + fun withConversationExtensionsReturningPager(kaliumPager: KaliumPager) = apply { + every { + conversationDaoExtensions.getPagerForConversationDetailsWithEventsSearch(any(), any(), any(), any(), any(), any()) + }.returns(kaliumPager) + } + fun arrange() = this to conversationRepositoryExtensions + } +} diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDAO.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDAO.kt index 734524cb16d..084dd6a8617 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDAO.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDAO.kt @@ -31,6 +31,7 @@ data class ProposalTimerEntity( @Suppress("TooManyFunctions") interface ConversationDAO { + val platformExtensions: ConversationExtensions //region Get/Observe by ID suspend fun observeConversationById(qualifiedID: QualifiedIDEntity): Flow diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDAOImpl.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDAOImpl.kt index 741e27498b0..8c67a860972 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDAOImpl.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDAOImpl.kt @@ -60,6 +60,8 @@ internal class ConversationDAOImpl internal constructor( ) : ConversationDAO { private val conversationMapper = ConversationMapper private val conversationDetailsWithEventsMapper = ConversationDetailsWithEventsMapper + override val platformExtensions: ConversationExtensions = + ConversationExtensionsImpl(conversationQueries, conversationDetailsWithEventsMapper, coroutineContext) // region Get/Observe by ID diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationExtensions.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationExtensions.kt new file mode 100644 index 00000000000..2aa3f5f695f --- /dev/null +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationExtensions.kt @@ -0,0 +1,74 @@ +/* + * 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.persistence.dao.conversation + +import app.cash.paging.Pager +import app.cash.paging.PagingConfig +import app.cash.sqldelight.paging3.QueryPagingSource +import com.wire.kalium.persistence.ConversationsQueries +import com.wire.kalium.persistence.dao.message.KaliumPager +import kotlin.coroutines.CoroutineContext + +interface ConversationExtensions { + fun getPagerForConversationDetailsWithEventsSearch( + pagingConfig: PagingConfig, + searchQuery: String = "", + fromArchive: Boolean = false, + onlyInteractionEnabled: Boolean = false, + newActivitiesOnTop: Boolean = false, + startingOffset: Long = 0, + ): KaliumPager +} + +internal class ConversationExtensionsImpl internal constructor( + private val queries: ConversationsQueries, + private val mapper: ConversationDetailsWithEventsMapper, + private val coroutineContext: CoroutineContext, +) : ConversationExtensions { + override fun getPagerForConversationDetailsWithEventsSearch( + pagingConfig: PagingConfig, + searchQuery: String, + fromArchive: Boolean, + onlyInteractionEnabled: Boolean, + newActivitiesOnTop: Boolean, + startingOffset: Long + ): KaliumPager = + KaliumPager( // We could return a Flow directly, but having the PagingSource is the only way to test this + Pager(pagingConfig) { pagingSource(searchQuery, fromArchive, onlyInteractionEnabled, newActivitiesOnTop, startingOffset) }, + pagingSource(searchQuery, fromArchive, onlyInteractionEnabled, newActivitiesOnTop, startingOffset), + coroutineContext + ) + + private fun pagingSource( + searchQuery: String, + fromArchive: Boolean, + onlyInteractionEnabled: Boolean, + newActivitiesOnTop: Boolean, + initialOffset: Long + ) = QueryPagingSource( + countQuery = queries.countConversationDetailsWithEventsFromSearch(fromArchive, onlyInteractionEnabled, searchQuery), + transacter = queries, + context = coroutineContext, + initialOffset = initialOffset, + queryProvider = { limit, offset -> + queries.selectConversationDetailsWithEventsFromSearch( + fromArchive, onlyInteractionEnabled, searchQuery, newActivitiesOnTop, limit, offset, mapper::fromViewToModel + ) + } + ) +} diff --git a/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/conversation/ConversationExtensionsTest.kt b/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/conversation/ConversationExtensionsTest.kt new file mode 100644 index 00000000000..6e3cab528e6 --- /dev/null +++ b/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/conversation/ConversationExtensionsTest.kt @@ -0,0 +1,197 @@ +/* + * 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.persistence.conversation + +import app.cash.paging.PagingConfig +import app.cash.paging.PagingSource +import app.cash.paging.PagingSourceLoadParamsAppend +import app.cash.paging.PagingSourceLoadParamsRefresh +import app.cash.paging.PagingSourceLoadResultPage +import com.wire.kalium.persistence.BaseDatabaseTest +import com.wire.kalium.persistence.dao.ConnectionDAO +import com.wire.kalium.persistence.dao.ConversationIDEntity +import com.wire.kalium.persistence.dao.UserDAO +import com.wire.kalium.persistence.dao.UserIDEntity +import com.wire.kalium.persistence.dao.conversation.ConversationDAO +import com.wire.kalium.persistence.dao.conversation.ConversationDetailsWithEventsMapper +import com.wire.kalium.persistence.dao.conversation.ConversationDetailsWithEventsEntity +import com.wire.kalium.persistence.dao.conversation.ConversationEntity +import com.wire.kalium.persistence.dao.conversation.ConversationExtensions +import com.wire.kalium.persistence.dao.conversation.ConversationExtensionsImpl +import com.wire.kalium.persistence.dao.member.MemberDAO +import com.wire.kalium.persistence.dao.message.KaliumPager +import com.wire.kalium.persistence.dao.message.MessageDAO +import com.wire.kalium.persistence.dao.message.draft.MessageDraftDAO +import com.wire.kalium.persistence.utils.stubs.newConversationEntity +import com.wire.kalium.persistence.utils.stubs.newUserEntity +import kotlinx.coroutines.test.runTest +import kotlinx.datetime.Instant +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertIs +import kotlin.test.assertTrue +import kotlin.time.Duration.Companion.seconds + +class ConversationExtensionsTest : BaseDatabaseTest() { + private lateinit var conversationExtensions: ConversationExtensions + private lateinit var messageDAO: MessageDAO + private lateinit var messageDraftDAO: MessageDraftDAO + private lateinit var conversationDAO: ConversationDAO + private lateinit var connectionDAO: ConnectionDAO + private lateinit var memberDAO: MemberDAO + private lateinit var userDAO: UserDAO + private val selfUserId = UserIDEntity("selfValue", "selfDomain") + + @BeforeTest + fun setUp() { + deleteDatabase(selfUserId) + val db = createDatabase(selfUserId, encryptedDBSecret, true) + val queries = db.database.conversationsQueries + messageDAO = db.messageDAO + messageDraftDAO = db.messageDraftDAO + conversationDAO = db.conversationDAO + connectionDAO = db.connectionDAO + memberDAO = db.memberDAO + userDAO = db.userDAO + conversationExtensions = ConversationExtensionsImpl(queries, ConversationDetailsWithEventsMapper, dispatcher) + } + + @AfterTest + fun tearDown() { + deleteDatabase(selfUserId) + } + + @Test + fun givenInsertedConversations_whenGettingFirstPage_thenItShouldContainTheCorrectCountBeforeAndAfter() = runTest(dispatcher) { + populateData() + val result = getPager().pagingSource.refresh() + assertIs>(result) + // Assuming the first page was fetched, itemsAfter should be the remaining ones + assertEquals(CONVERSATION_COUNT - PAGE_SIZE, result.itemsAfter) + assertEquals(0, result.itemsBefore) // No items before the first page + } + + @Test + fun givenInsertedConversations_whenGettingFirstSearchedPage_thenItShouldContainTheCorrectCountBeforeAndAfter() = runTest(dispatcher) { + populateData() + val searchQuery = "conversation 1" + val result = getPager(searchQuery = searchQuery).pagingSource.refresh() + assertIs>(result) + // Assuming the first page was fetched containing only 11 results ("conversation 1" and "conversation 10" to "conversation 19") + assertEquals(0, result.itemsAfter) // Since the page has fewer elements than PAGE_SIZE, there should be no items after this page + assertEquals(0, result.itemsBefore) // No items before the first page + } + + @Test + fun givenInsertedConversations_whenGettingFirstPage_thenTheNextKeyShouldBeTheFirstItemOfTheNextPage() = runTest(dispatcher) { + populateData() + val result = getPager().pagingSource.refresh() + assertIs>(result) + assertEquals(PAGE_SIZE, result.nextKey) // First page fetched, second page starts at the end of the first one + } + + @Test + fun givenInsertedConversations_whenGettingFirstPage_thenItShouldContainTheFirstPageOfItems() = runTest(dispatcher) { + populateData() + val result = getPager().pagingSource.refresh() + assertIs>(result) + result.data.forEachIndexed { index, conversation -> + assertEquals("$CONVERSATION_ID_PREFIX$index", conversation.conversationViewEntity.id.value) + assertEquals(false, conversation.conversationViewEntity.archived) + } + } + + @Test + fun givenInsertedConversations_whenGettingSecondPage_thenShouldContainTheCorrectItems() = runTest(dispatcher) { + populateData() + val pagingSource = getPager().pagingSource + val secondPageResult = pagingSource.nextPageForOffset(PAGE_SIZE) + assertIs>(secondPageResult) + assertFalse { secondPageResult.data.isEmpty() } + assertTrue { secondPageResult.data.size <= PAGE_SIZE } + secondPageResult.data.forEachIndexed { index, conversation -> + assertEquals("$CONVERSATION_ID_PREFIX${index + PAGE_SIZE}", conversation.conversationViewEntity.id.value) + } + } + + @Test + fun givenInsertedConversations_whenGettingFirstPageOfArchivedConversations_thenItShouldContainArchivedItems() = runTest(dispatcher) { + populateData(archived = false, count = CONVERSATION_COUNT, conversationIdPrefix = CONVERSATION_ID_PREFIX) + populateData(archived = true, count = CONVERSATION_COUNT, conversationIdPrefix = ARCHIVED_CONVERSATION_ID_PREFIX) + val result = getPager(fromArchive = true).pagingSource.refresh() + assertIs>(result) + result.data.forEachIndexed { index, conversation -> + assertEquals("$ARCHIVED_CONVERSATION_ID_PREFIX$index", conversation.conversationViewEntity.id.value) + assertEquals(true, conversation.conversationViewEntity.archived) + } + } + + @Test + fun givenInsertedConversations_whenGettingFirstSearchedPage_thenShouldContainTheCorrectItems() = runTest(dispatcher) { + populateData() + val searchQuery = "conversation 1" + val result = getPager(searchQuery = searchQuery).pagingSource.refresh() + assertIs>(result) + // Assuming the first page was fetched containing only 11 results ["conversation 1" and "conversation 10" to "conversation 19"] + assertEquals(11, result.data.size) + result.data.forEachIndexed { index, conversation -> + assertEquals(true, conversation.conversationViewEntity.name?.contains(searchQuery) ?: false) + } + } + + private fun getPager(searchQuery: String = "", fromArchive: Boolean = false): KaliumPager = + conversationExtensions.getPagerForConversationDetailsWithEventsSearch(PagingConfig(PAGE_SIZE), searchQuery, fromArchive) + + private suspend fun PagingSource.refresh() = + load(PagingSourceLoadParamsRefresh(null, PAGE_SIZE, false)) + + private suspend fun PagingSource.nextPageForOffset(key: Int) = + load(PagingSourceLoadParamsAppend(key, PAGE_SIZE, true)) + + private suspend fun populateData( + archived: Boolean = false, + count: Int = CONVERSATION_COUNT, + conversationIdPrefix: String = CONVERSATION_ID_PREFIX, + ) { + userDAO.upsertUser(newUserEntity(qualifiedID = UserIDEntity("user", "domain"))) + repeat(count) { + // Ordered by date - Inserting with decreasing date is important to assert pagination + val lastModified = Instant.fromEpochSeconds(CONVERSATION_COUNT - it.toLong()) + val lastRead = lastModified.minus(1.seconds) // if message needs to be unread, then lastRead should be before lastModified + val conversation = newConversationEntity(ConversationIDEntity("$conversationIdPrefix$it", "domain")).copy( + name = "conversation $it", + type = ConversationEntity.Type.GROUP, + lastModifiedDate = lastModified, + lastReadDate = lastRead, + archived = archived, + ) + conversationDAO.insertConversation(conversation) + } + } + + private companion object { + const val CONVERSATION_COUNT = 100 + const val CONVERSATION_ID_PREFIX = "conversation_" + const val ARCHIVED_CONVERSATION_ID_PREFIX = "archived_conversation_" + const val PAGE_SIZE = 20 + } +} From a2575041c6b95be7e67c280b0c56d5e8d1d01ee1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Saleniuk?= Date: Wed, 9 Oct 2024 14:42:39 +0200 Subject: [PATCH 3/6] fixed detekt issues --- .../dao/conversation/ConversationDAOImpl.kt | 5 +++- .../ConversationDetailsWithEventsMapper.kt | 23 +++++++++++-------- .../dao/conversation/ConversationMapper.kt | 9 ++++---- 3 files changed, 23 insertions(+), 14 deletions(-) diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDAOImpl.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDAOImpl.kt index 741e27498b0..0a38f14f230 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDAOImpl.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDAOImpl.kt @@ -221,7 +221,10 @@ internal class ConversationDAOImpl internal constructor( newActivitiesOnTop: Boolean, ): Flow> { return conversationQueries.selectAllConversationDetailsWithEvents( - fromArchive, onlyInteractionEnabled, newActivitiesOnTop, conversationDetailsWithEventsMapper::fromViewToModel + fromArchive = fromArchive, + onlyInteractionsEnabled = onlyInteractionEnabled, + newActivitiesOnTop = newActivitiesOnTop, + mapper = conversationDetailsWithEventsMapper::fromViewToModel ).asFlow() .mapToList() .flowOn(coroutineContext) diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDetailsWithEventsMapper.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDetailsWithEventsMapper.kt index 4f18ed7390b..599a41b2f96 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDetailsWithEventsMapper.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDetailsWithEventsMapper.kt @@ -32,7 +32,8 @@ import com.wire.kalium.persistence.dao.unread.UnreadEventMapper import kotlinx.datetime.Instant data object ConversationDetailsWithEventsMapper { - @Suppress("LongParameterList", "FunctionParameterNaming") + // suppressed because the method cannot be shortened and there are unused parameters because sql view returns some duplicated fields + @Suppress("LongParameterList", "LongMethod", "UnusedParameter") fun fromViewToModel( qualifiedId: QualifiedIDEntity, name: String?, @@ -41,7 +42,7 @@ data object ConversationDetailsWithEventsMapper { previewAssetId: QualifiedIDEntity?, mutedStatus: ConversationEntity.MutedStatus, teamId: String?, - lastModifiedDate_: Instant?, + lastModifiedDate: Instant?, lastReadDate: Instant, userAvailabilityStatus: UserAvailabilityStatusEntity?, userType: UserTypeEntity?, @@ -65,11 +66,11 @@ data object ConversationDetailsWithEventsMapper { mlsGroupState: ConversationEntity.GroupState, accessList: List, accessRoleList: List, - teamId_: String?, + unusedTeamId: String?, mlsProposalTimer: String?, mutedTime: Long, creatorId: String, - lastModifiedDate: Instant, + unusedLastModifiedDate: Instant, receiptMode: ConversationEntity.ReceiptMode, messageTimer: Long?, userMessageTimer: Long?, @@ -122,7 +123,7 @@ data object ConversationDetailsWithEventsMapper { previewAssetId = previewAssetId, mutedStatus = mutedStatus, teamId = teamId, - lastModifiedDate_ = lastModifiedDate_, + lastModifiedDate = lastModifiedDate, lastReadDate = lastReadDate, userAvailabilityStatus = userAvailabilityStatus, userType = userType, @@ -146,11 +147,11 @@ data object ConversationDetailsWithEventsMapper { mlsGroupState = mlsGroupState, accessList = accessList, accessRoleList = accessRoleList, - teamId_ = teamId_, + unusedTeamId = unusedTeamId, mlsProposalTimer = mlsProposalTimer, mutedTime = mutedTime, creatorId = creatorId, - lastModifiedDate = lastModifiedDate, + unusedLastModifiedDate = unusedLastModifiedDate, receiptMode = receiptMode, messageTimer = messageTimer, userMessageTimer = userMessageTimer, @@ -169,10 +170,14 @@ data object ConversationDetailsWithEventsMapper { repliesCount = unreadRepliesCount, messagesCount = unreadMessagesCount, ), - lastMessage = if (lastMessageId != null && lastMessageContentType != null && lastMessageDate != null + lastMessage = + @Suppress("ComplexCondition") // we need to check all these fields + if ( + lastMessageId != null && lastMessageContentType != null && lastMessageDate != null && lastMessageVisibility != null && lastMessageSenderUserId != null && lastMessageIsEphemeral != null && lastMessageIsSelfMessage != null && lastMessageIsMentioningSelfUser != null && lastMessageIsUnread != null - && lastMessageShouldNotify != null) { + && lastMessageShouldNotify != null + ) { MessageMapper.toPreviewEntity( id = lastMessageId, conversationId = qualifiedId, diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationMapper.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationMapper.kt index d18ce129d2a..306abc1ed6e 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationMapper.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationMapper.kt @@ -28,7 +28,8 @@ import com.wire.kalium.persistence.dao.member.MemberEntity import kotlinx.datetime.Instant data object ConversationMapper { - @Suppress("LongParameterList", "UnusedParameter", "FunctionParameterNaming") + // suppressed because the method cannot be shortened and there are unused parameters because sql view returns some duplicated fields + @Suppress("LongParameterList", "LongMethod", "UnusedParameter") fun fromViewToModel( qualifiedId: QualifiedIDEntity, name: String?, @@ -37,7 +38,7 @@ data object ConversationMapper { previewAssetId: QualifiedIDEntity?, mutedStatus: ConversationEntity.MutedStatus, teamId: String?, - lastModifiedDate_: Instant?, + lastModifiedDate: Instant?, lastReadDate: Instant, userAvailabilityStatus: UserAvailabilityStatusEntity?, userType: UserTypeEntity?, @@ -61,11 +62,11 @@ data object ConversationMapper { mlsGroupState: ConversationEntity.GroupState, accessList: List, accessRoleList: List, - teamId_: String?, + unusedTeamId: String?, mlsProposalTimer: String?, mutedTime: Long, creatorId: String, - lastModifiedDate: Instant, + unusedLastModifiedDate: Instant, receiptMode: ConversationEntity.ReceiptMode, messageTimer: Long?, userMessageTimer: Long?, From 24b08ac410e992fcc1a005ecc13e677a3082a13b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Saleniuk?= Date: Mon, 14 Oct 2024 14:50:19 +0200 Subject: [PATCH 4/6] fixed detekt issues --- ...onDetailsWithEventsBySearchQueryUseCase.kt | 12 ++-- .../ConversationRepositoryExtensions.kt | 24 ++++--- .../conversation/ConversationExtensions.kt | 65 ++++++++++--------- 3 files changed, 54 insertions(+), 47 deletions(-) diff --git a/logic/src/androidMain/kotlin/com/wire/kalium/logic/feature/conversation/GetPaginatedFlowOfConversationDetailsWithEventsBySearchQueryUseCase.kt b/logic/src/androidMain/kotlin/com/wire/kalium/logic/feature/conversation/GetPaginatedFlowOfConversationDetailsWithEventsBySearchQueryUseCase.kt index f01ce1ad0ed..61bf99999e3 100644 --- a/logic/src/androidMain/kotlin/com/wire/kalium/logic/feature/conversation/GetPaginatedFlowOfConversationDetailsWithEventsBySearchQueryUseCase.kt +++ b/logic/src/androidMain/kotlin/com/wire/kalium/logic/feature/conversation/GetPaginatedFlowOfConversationDetailsWithEventsBySearchQueryUseCase.kt @@ -21,6 +21,7 @@ import androidx.paging.PagingConfig import androidx.paging.PagingData import com.wire.kalium.logic.data.conversation.ConversationDetailsWithEvents import com.wire.kalium.logic.data.conversation.ConversationRepository +import com.wire.kalium.logic.data.conversation.ConversationQueryConfig import com.wire.kalium.util.KaliumDispatcher import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOn @@ -35,14 +36,9 @@ class GetPaginatedFlowOfConversationDetailsWithEventsBySearchQueryUseCase intern private val conversationRepository: ConversationRepository, ) { suspend operator fun invoke( - searchQuery: String, - fromArchive: Boolean, - onlyInteractionsEnabled: Boolean, - newActivitiesOnTop: Boolean, + queryConfig: ConversationQueryConfig, + pagingConfig: PagingConfig, startingOffset: Long, - pagingConfig: PagingConfig ): Flow> = conversationRepository.extensions - .getPaginatedConversationDetailsWithEventsBySearchQuery( - searchQuery, fromArchive, onlyInteractionsEnabled, newActivitiesOnTop, pagingConfig, startingOffset - ).flowOn(dispatcher.io) + .getPaginatedConversationDetailsWithEventsBySearchQuery(queryConfig, pagingConfig, startingOffset).flowOn(dispatcher.io) } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepositoryExtensions.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepositoryExtensions.kt index 6e7d70cc0d9..549b365abeb 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepositoryExtensions.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepositoryExtensions.kt @@ -24,6 +24,7 @@ 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 @@ -31,10 +32,7 @@ import kotlinx.coroutines.flow.map interface ConversationRepositoryExtensions { suspend fun getPaginatedConversationDetailsWithEventsBySearchQuery( - searchQuery: String, - fromArchive: Boolean, - onlyInteractionsEnabled: Boolean, - newActivitiesOnTop: Boolean, + queryConfig: ConversationQueryConfig, pagingConfig: PagingConfig, startingOffset: Long, ): Flow> @@ -46,17 +44,16 @@ class ConversationRepositoryExtensionsImpl internal constructor( private val messageMapper: MessageMapper, ) : ConversationRepositoryExtensions { override suspend fun getPaginatedConversationDetailsWithEventsBySearchQuery( - searchQuery: String, - fromArchive: Boolean, - onlyInteractionsEnabled: Boolean, - newActivitiesOnTop: Boolean, + queryConfig: ConversationQueryConfig, pagingConfig: PagingConfig, startingOffset: Long ): Flow> { - val pager: KaliumPager = + val pager: KaliumPager = with(queryConfig) { conversationDAO.platformExtensions.getPagerForConversationDetailsWithEventsSearch( - pagingConfig, searchQuery, fromArchive, onlyInteractionsEnabled, newActivitiesOnTop, startingOffset + queryConfig = QueryConfig(searchQuery, fromArchive, onlyInteractionEnabled, newActivitiesOnTop), + pagingConfig = pagingConfig ) + } return pager.pagingDataFlow.map { it.map { @@ -82,3 +79,10 @@ class ConversationRepositoryExtensionsImpl internal constructor( } } } + +data class ConversationQueryConfig( + val searchQuery: String = "", + val fromArchive: Boolean = false, + val onlyInteractionEnabled: Boolean = false, + val newActivitiesOnTop: Boolean = false, +) diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationExtensions.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationExtensions.kt index 2aa3f5f695f..cda01b14727 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationExtensions.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationExtensions.kt @@ -21,18 +21,23 @@ import app.cash.paging.Pager import app.cash.paging.PagingConfig import app.cash.sqldelight.paging3.QueryPagingSource import com.wire.kalium.persistence.ConversationsQueries +import com.wire.kalium.persistence.dao.conversation.ConversationExtensions.QueryConfig import com.wire.kalium.persistence.dao.message.KaliumPager import kotlin.coroutines.CoroutineContext interface ConversationExtensions { fun getPagerForConversationDetailsWithEventsSearch( + queryConfig: QueryConfig, pagingConfig: PagingConfig, - searchQuery: String = "", - fromArchive: Boolean = false, - onlyInteractionEnabled: Boolean = false, - newActivitiesOnTop: Boolean = false, startingOffset: Long = 0, ): KaliumPager + + data class QueryConfig( + val searchQuery: String = "", + val fromArchive: Boolean = false, + val onlyInteractionEnabled: Boolean = false, + val newActivitiesOnTop: Boolean = false, + ) } internal class ConversationExtensionsImpl internal constructor( @@ -41,34 +46,36 @@ internal class ConversationExtensionsImpl internal constructor( private val coroutineContext: CoroutineContext, ) : ConversationExtensions { override fun getPagerForConversationDetailsWithEventsSearch( + queryConfig: QueryConfig, pagingConfig: PagingConfig, - searchQuery: String, - fromArchive: Boolean, - onlyInteractionEnabled: Boolean, - newActivitiesOnTop: Boolean, startingOffset: Long ): KaliumPager = - KaliumPager( // We could return a Flow directly, but having the PagingSource is the only way to test this - Pager(pagingConfig) { pagingSource(searchQuery, fromArchive, onlyInteractionEnabled, newActivitiesOnTop, startingOffset) }, - pagingSource(searchQuery, fromArchive, onlyInteractionEnabled, newActivitiesOnTop, startingOffset), - coroutineContext + KaliumPager( + // We could return a Flow directly, but having the PagingSource is the only way to test this + pager = Pager(pagingConfig) { + pagingSource(queryConfig, startingOffset) + }, + pagingSource = pagingSource(queryConfig, startingOffset), + coroutineContext = coroutineContext, ) - private fun pagingSource( - searchQuery: String, - fromArchive: Boolean, - onlyInteractionEnabled: Boolean, - newActivitiesOnTop: Boolean, - initialOffset: Long - ) = QueryPagingSource( - countQuery = queries.countConversationDetailsWithEventsFromSearch(fromArchive, onlyInteractionEnabled, searchQuery), - transacter = queries, - context = coroutineContext, - initialOffset = initialOffset, - queryProvider = { limit, offset -> - queries.selectConversationDetailsWithEventsFromSearch( - fromArchive, onlyInteractionEnabled, searchQuery, newActivitiesOnTop, limit, offset, mapper::fromViewToModel - ) - } - ) + private fun pagingSource(queryConfig: QueryConfig, initialOffset: Long) = with(queryConfig) { + QueryPagingSource( + countQuery = queries.countConversationDetailsWithEventsFromSearch(fromArchive, onlyInteractionEnabled, searchQuery), + transacter = queries, + context = coroutineContext, + initialOffset = initialOffset, + queryProvider = { limit, offset -> + queries.selectConversationDetailsWithEventsFromSearch( + fromArchive = fromArchive, + onlyInteractionsEnabled = onlyInteractionEnabled, + searchQuery = searchQuery, + newActivitiesOnTop = newActivitiesOnTop, + limit = limit, + offset = offset, + mapper = mapper::fromViewToModel, + ) + } + ) + } } From 4fd92b446a4b2ea0c7b42cf8c87d01a1eeb9da19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Saleniuk?= Date: Mon, 14 Oct 2024 15:01:51 +0200 Subject: [PATCH 5/6] fixed tests --- .../ConversationRepositoryExtensionsTest.kt | 26 ++++++++++++++----- .../ConversationExtensionsTest.kt | 5 +++- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepositoryExtensionsTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepositoryExtensionsTest.kt index c04e8d736d8..82ee1f9d0aa 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepositoryExtensionsTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepositoryExtensionsTest.kt @@ -33,6 +33,7 @@ import io.mockative.Mock import io.mockative.any import io.mockative.eq import io.mockative.every +import io.mockative.matches import io.mockative.mock import io.mockative.once import io.mockative.verify @@ -58,26 +59,37 @@ class ConversationRepositoryExtensionsTest { .arrange() val searchQuery = "search" conversationRepositoryExtensions.getPaginatedConversationDetailsWithEventsBySearchQuery( - searchQuery = searchQuery, - fromArchive = false, - onlyInteractionsEnabled = false, - newActivitiesOnTop = false, + queryConfig = ConversationQueryConfig( + searchQuery = searchQuery, + fromArchive = false, + onlyInteractionEnabled = false, + newActivitiesOnTop = false, + ), pagingConfig = pagingConfig, startingOffset = 0L ) verify { arrangement.conversationDaoExtensions - .getPagerForConversationDetailsWithEventsSearch(eq(pagingConfig), eq(searchQuery), eq(false), eq(false), eq(false), any()) + .getPagerForConversationDetailsWithEventsSearch( + queryConfig = matches { + it.searchQuery == searchQuery && !it.fromArchive && !it.onlyInteractionEnabled && !it.newActivitiesOnTop + }, + pagingConfig = eq(pagingConfig), + startingOffset = any() + ) }.wasInvoked(exactly = once) } private class Arrangement { @Mock val conversationDaoExtensions: ConversationExtensions = mock(ConversationExtensions::class) + @Mock private val conversationDAO: ConversationDAO = mock(ConversationDAO::class) + @Mock private val conversationMapper: ConversationMapper = mock(ConversationMapper::class) + @Mock private val messageMapper: MessageMapper = mock(MessageMapper::class) private val conversationRepositoryExtensions: ConversationRepositoryExtensions by lazy { @@ -95,11 +107,13 @@ class ConversationRepositoryExtensionsTest { conversationDAO.platformExtensions }.returns(conversationDaoExtensions) } + fun withConversationExtensionsReturningPager(kaliumPager: KaliumPager) = apply { every { - conversationDaoExtensions.getPagerForConversationDetailsWithEventsSearch(any(), any(), any(), any(), any(), any()) + conversationDaoExtensions.getPagerForConversationDetailsWithEventsSearch(any(), any(), any()) }.returns(kaliumPager) } + fun arrange() = this to conversationRepositoryExtensions } } diff --git a/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/conversation/ConversationExtensionsTest.kt b/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/conversation/ConversationExtensionsTest.kt index 6e3cab528e6..95610f7f3f6 100644 --- a/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/conversation/ConversationExtensionsTest.kt +++ b/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/conversation/ConversationExtensionsTest.kt @@ -159,7 +159,10 @@ class ConversationExtensionsTest : BaseDatabaseTest() { } private fun getPager(searchQuery: String = "", fromArchive: Boolean = false): KaliumPager = - conversationExtensions.getPagerForConversationDetailsWithEventsSearch(PagingConfig(PAGE_SIZE), searchQuery, fromArchive) + conversationExtensions.getPagerForConversationDetailsWithEventsSearch( + pagingConfig = PagingConfig(PAGE_SIZE), + queryConfig = ConversationExtensions.QueryConfig(searchQuery = searchQuery, fromArchive = fromArchive), + ) private suspend fun PagingSource.refresh() = load(PagingSourceLoadParamsRefresh(null, PAGE_SIZE, false)) From 23e2bd48fdb1aabaf16b7a6a463864eee82e7739 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Saleniuk?= Date: Mon, 14 Oct 2024 16:59:09 +0200 Subject: [PATCH 6/6] added test for paginated use case --- ...tailsWithEventsBySearchQueryUseCaseTest.kt | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 logic/src/androidUnitTest/kotlin/com/wire/kalium/logic/feature/conversation/GetPaginatedFlowOfConversationDetailsWithEventsBySearchQueryUseCaseTest.kt diff --git a/logic/src/androidUnitTest/kotlin/com/wire/kalium/logic/feature/conversation/GetPaginatedFlowOfConversationDetailsWithEventsBySearchQueryUseCaseTest.kt b/logic/src/androidUnitTest/kotlin/com/wire/kalium/logic/feature/conversation/GetPaginatedFlowOfConversationDetailsWithEventsBySearchQueryUseCaseTest.kt new file mode 100644 index 00000000000..31ace8e2c65 --- /dev/null +++ b/logic/src/androidUnitTest/kotlin/com/wire/kalium/logic/feature/conversation/GetPaginatedFlowOfConversationDetailsWithEventsBySearchQueryUseCaseTest.kt @@ -0,0 +1,82 @@ +/* + * 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 + +import androidx.paging.PagingData +import app.cash.paging.PagingConfig +import com.wire.kalium.logic.data.conversation.ConversationDetailsWithEvents +import com.wire.kalium.logic.data.conversation.ConversationQueryConfig +import com.wire.kalium.logic.data.conversation.ConversationRepository +import com.wire.kalium.logic.data.conversation.ConversationRepositoryExtensions +import com.wire.kalium.logic.test_util.TestKaliumDispatcher +import io.mockative.Mock +import io.mockative.any +import io.mockative.coEvery +import io.mockative.coVerify +import io.mockative.every +import io.mockative.mock +import io.mockative.once +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class GetPaginatedFlowOfConversationDetailsWithEventsBySearchQueryUseCaseTest { + private val dispatcher = TestKaliumDispatcher + + @Test + fun givenSearchQuery_whenGettingPaginatedList_thenCallUseCaseWithProperParams() = runTest(dispatcher.default) { + // Given + val (arrangement, useCase) = Arrangement().withPaginatedConversationResult(emptyFlow()).arrange() + with(arrangement) { + // When + useCase(queryConfig = queryConfig, pagingConfig = pagingConfig, startingOffset = startingOffset) + // Then + coVerify { + conversationRepository.extensions + .getPaginatedConversationDetailsWithEventsBySearchQuery(queryConfig, pagingConfig, startingOffset) + }.wasInvoked(exactly = once) + } + } + + inner class Arrangement { + @Mock + val conversationRepository = mock(ConversationRepository::class) + + @Mock + val conversationRepositoryExtensions = mock(ConversationRepositoryExtensions::class) + + val queryConfig = ConversationQueryConfig("search") + val pagingConfig = PagingConfig(20) + val startingOffset = 0L + + init { + every { + conversationRepository.extensions + }.returns(conversationRepositoryExtensions) + } + + suspend fun withPaginatedConversationResult(result: Flow>) = apply { + coEvery { + conversationRepositoryExtensions.getPaginatedConversationDetailsWithEventsBySearchQuery(any(), any(), any()) + }.returns(result) + } + + fun arrange() = this to GetPaginatedFlowOfConversationDetailsWithEventsBySearchQueryUseCase(dispatcher, conversationRepository) + } +}