diff --git a/calling/consumer-proguard-rules.pro b/calling/consumer-proguard-rules.pro index eed5182980c..3825933e243 100644 --- a/calling/consumer-proguard-rules.pro +++ b/calling/consumer-proguard-rules.pro @@ -6,6 +6,9 @@ -keep class com.waz.call.CaptureDevice { *; } -keep class com.waz.media.manager.** { *; } -keep class com.waz.service.call.** { *; } +-dontwarn org.webrtc.CalledByNative +-dontwarn org.webrtc.JniCommon +-dontwarn org.webrtc.audio.AudioDeviceModule # Avs SoundLink -keep class com.waz.soundlink.SoundLinkAPI { *; } diff --git a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/id/QualifiedId.kt b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/id/QualifiedId.kt index a8f19d888f1..d6996ecb13f 100644 --- a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/id/QualifiedId.kt +++ b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/id/QualifiedId.kt @@ -33,7 +33,8 @@ data class QualifiedID( @SerialName("domain") val domain: String ) { - override fun toString(): String = if (domain.isEmpty()) value else "$value$VALUE_DOMAIN_SEPARATOR$domain" + override fun toString(): String = + if (domain.isEmpty()) value else "$value$VALUE_DOMAIN_SEPARATOR$domain" fun toLogString(): String = if (domain.isEmpty()) { value.obfuscateId() @@ -43,6 +44,16 @@ data class QualifiedID( fun toPlainID(): PlainId = PlainId(value) + /** + * This checks if the domain in either instances is blank. If it is, it will compare only the value. + * To be used when when of the instance do not have domain due to the API limitations. + */ + fun equalsIgnoringBlankDomain(other: QualifiedID): Boolean { + if (domain.isBlank() || other.domain.isBlank()) { + return value == other.value + } + return this == other + } } const val VALUE_DOMAIN_SEPARATOR = '@' diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d88cbbf7c86..9c1150c1735 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -37,7 +37,7 @@ sqldelight = "2.0.1" sqlcipher-android = "4.6.1" pbandk = "0.14.2" turbine = "1.1.0" -avs = "9.10.14" +avs = "9.10.16" jna = "5.14.0" core-crypto = "1.0.2" core-crypto-multiplatform = "0.6.0-rc.3-multiplatform-pre1" 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 1e86404efe9..f5283b9d389 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 @@ -70,15 +70,20 @@ import com.wire.kalium.network.api.base.authenticated.conversation.ConversationA import com.wire.kalium.persistence.dao.QualifiedIDEntity import com.wire.kalium.persistence.dao.client.ClientDAO 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.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.ConversationUnreadEventEntity 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.flowOf import kotlinx.coroutines.flow.map import kotlinx.datetime.Instant @@ -127,11 +132,7 @@ 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 observeConversationListDetailsWithEvents(fromArchive: Boolean = false): Flow> suspend fun getConversationIds( type: Conversation.Type, protocol: Conversation.Protocol, @@ -317,6 +318,7 @@ internal class ConversationDataSource internal constructor( private val memberDAO: MemberDAO, private val conversationApi: ConversationApi, private val messageDAO: MessageDAO, + private val messageDraftDAO: MessageDraftDAO, private val clientDAO: ClientDAO, private val clientApi: ClientApi, private val conversationMetaDataDAO: ConversationMetaDataDAO, @@ -520,17 +522,28 @@ internal class ConversationDataSource internal constructor( 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 observeConversationListDetailsWithEvents(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 } + val unreadEventsMap = unreadEvents.associateBy { it.conversationId } + + conversationList.map { conversation -> + conversationMapper.fromDaoModelToDetailsWithEvents( + ConversationDetailsWithEventsEntity( + conversationViewEntity = conversation, + lastMessage = lastMessageMap[conversation.id], + messageDraft = messageDraftMap[conversation.id], + unreadEvents = unreadEventsMap[conversation.id] ?: ConversationUnreadEventEntity(conversation.id, mapOf()), + ) + ) } + } override suspend fun fetchMlsOneToOneConversation(userId: UserId): Either = wrapApiRequest { @@ -983,7 +996,7 @@ internal class ConversationDataSource internal constructor( } override suspend fun getConversationDetailsByMLSGroupId(mlsGroupId: GroupID): Either = - wrapStorageRequest { conversationDAO.getConversationByGroupID(mlsGroupId.value) } + wrapStorageRequest { conversationDAO.getConversationDetailsByGroupID(mlsGroupId.value) } .map { conversationMapper.fromDaoModelToDetails(it) } override suspend fun observeUnreadArchivedConversationsCount(): Flow = 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 859269f102c..7351839496c 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 @@ -707,6 +707,7 @@ class UserSessionScope internal constructor( userStorage.database.memberDAO, authenticatedNetworkContainer.conversationApi, userStorage.database.messageDAO, + userStorage.database.messageDraftDAO, userStorage.database.clientDAO, authenticatedNetworkContainer.clientApi, userStorage.database.conversationMetaDataDAO, diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/backup/RestoreBackupUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/backup/RestoreBackupUseCase.kt index ab95181d207..cb2c5c105ac 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/backup/RestoreBackupUseCase.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/backup/RestoreBackupUseCase.kt @@ -95,7 +95,13 @@ internal class RestoreBackupUseCaseImpl( importEncryptedBackup(extractedBackupRootPath, password) } } - .fold({ it }, { RestoreBackupResult.Success }) + .fold({ error -> + kaliumLogger.e("$TAG Failed to restore the backup, reason: ${error.failure}") + error + }, { + kaliumLogger.i("$TAG Backup restored successfully") + RestoreBackupResult.Success + }) } private suspend fun importUnencryptedBackup( @@ -140,7 +146,7 @@ internal class RestoreBackupUseCaseImpl( val extractedFilesRootPath = createExtractedFilesRootPath() return extractFiles(tempCompressedFileSource, extractedFilesRootPath) .fold({ - kaliumLogger.e("Failed to extract backup files") + kaliumLogger.e("$TAG Failed to extract backup files") Either.Left(Failure(BackupIOFailure("Failed to extract backup files"))) }, { Either.Right(extractedFilesRootPath) @@ -176,7 +182,7 @@ internal class RestoreBackupUseCaseImpl( return if (backupSize > 0) { // On successful decryption, we still need to extract the zip file to do sanity checks and get the database file extractFiles(kaliumFileSystem.source(extractedBackupPath), extractedBackupRootPath).fold({ - kaliumLogger.e("Failed to extract encrypted backup files") + kaliumLogger.e("$TAG Failed to extract encrypted backup files") Either.Left(Failure(BackupIOFailure("Failed to extract encrypted backup files"))) }, { kaliumFileSystem.delete(extractedBackupPath) diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/backup/RestoreWebBackupUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/backup/RestoreWebBackupUseCase.kt index a5732a7a2be..be85aeaa09e 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/backup/RestoreWebBackupUseCase.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/backup/RestoreWebBackupUseCase.kt @@ -79,7 +79,13 @@ internal class RestoreWebBackupUseCaseImpl( importWebBackup(backupRootPath, this) } else { Either.Left(IncompatibleBackup("invoke: The provided backup format is not supported")) - }.fold({ RestoreBackupResult.Failure(it) }, { RestoreBackupResult.Success }) + }.fold({ error -> + kaliumLogger.e("$TAG Failed to restore the backup, reason: $error") + RestoreBackupResult.Failure(error) + }, { + kaliumLogger.i("$TAG Successfully restored the backup") + RestoreBackupResult.Success + }) } private suspend fun importWebBackup( 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 b471f6c742c..7ee88554ded 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 @@ -118,6 +118,9 @@ class ConversationScope internal constructor( val observeConversationListDetails: ObserveConversationListDetailsUseCase get() = ObserveConversationListDetailsUseCaseImpl(conversationRepository) + val observeConversationListDetailsWithEvents: ObserveConversationListDetailsWithEventsUseCase + get() = ObserveConversationListDetailsWithEventsUseCaseImpl(conversationRepository) + val observeConversationMembers: ObserveConversationMembersUseCase get() = ObserveConversationMembersUseCaseImpl(conversationRepository, userRepository) diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ObserveConversationListDetailsWithEventsUseCaseImpl.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ObserveConversationListDetailsWithEventsUseCaseImpl.kt new file mode 100644 index 00000000000..543ca5d213c --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ObserveConversationListDetailsWithEventsUseCaseImpl.kt @@ -0,0 +1,41 @@ +/* + * 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 com.wire.kalium.logic.data.conversation.ConversationDetails +import com.wire.kalium.logic.data.conversation.ConversationDetailsWithEvents +import com.wire.kalium.logic.data.conversation.ConversationRepository +import kotlinx.coroutines.flow.Flow + +/** + * This use case will observe and return the list of conversation details for the current user. + * @see ConversationDetails + */ +fun interface ObserveConversationListDetailsWithEventsUseCase { + suspend operator fun invoke(fromArchive: Boolean): Flow> +} + +internal class ObserveConversationListDetailsWithEventsUseCaseImpl( + private val conversationRepository: ConversationRepository, +) : ObserveConversationListDetailsWithEventsUseCase { + + override suspend operator fun invoke(fromArchive: Boolean): Flow> { + return conversationRepository.observeConversationListDetailsWithEvents(fromArchive) + } +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/user/UploadUserAvatarUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/user/UploadUserAvatarUseCase.kt index b7fc56a5503..8bbc0e12e04 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/user/UploadUserAvatarUseCase.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/user/UploadUserAvatarUseCase.kt @@ -25,6 +25,8 @@ import com.wire.kalium.logic.data.user.UserRepository import com.wire.kalium.logic.functional.flatMap import com.wire.kalium.logic.functional.fold import com.wire.kalium.logic.functional.map +import com.wire.kalium.util.KaliumDispatcherImpl +import kotlinx.coroutines.withContext import okio.Path interface UploadUserAvatarUseCase { @@ -40,17 +42,20 @@ interface UploadUserAvatarUseCase { internal class UploadUserAvatarUseCaseImpl( private val userDataSource: UserRepository, - private val assetDataSource: AssetRepository + private val assetDataSource: AssetRepository, + private val dispatcher: KaliumDispatcherImpl = KaliumDispatcherImpl ) : UploadUserAvatarUseCase { override suspend operator fun invoke(imageDataPath: Path, imageDataSize: Long): UploadAvatarResult { - return assetDataSource.uploadAndPersistPublicAsset("image/jpg", imageDataPath, imageDataSize).flatMap { asset -> - userDataSource.updateSelfUser(newAssetId = asset.key).map { asset } - }.fold({ - UploadAvatarResult.Failure(it) - }) { updatedAsset -> - UploadAvatarResult.Success(UserAssetId(updatedAsset.key, updatedAsset.domain)) - } // TODO(assets): remove old assets, non blocking this response, as will imply deleting locally and remotely + return withContext(dispatcher.io) { + assetDataSource.uploadAndPersistPublicAsset("image/jpg", imageDataPath, imageDataSize).flatMap { asset -> + userDataSource.updateSelfUser(newAssetId = asset.key).map { asset } + }.fold({ + UploadAvatarResult.Failure(it) + }) { updatedAsset -> + UploadAvatarResult.Success(UserAssetId(updatedAsset.key, updatedAsset.domain)) + } // TODO(assets): remove old assets, non blocking this response, as will imply deleting locally and remotely + } } } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/SyncManagerLogger.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/SyncManagerLogger.kt new file mode 100644 index 00000000000..145a813480e --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/SyncManagerLogger.kt @@ -0,0 +1,99 @@ +/* + * 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.sync + +import com.benasher44.uuid.uuid4 +import com.wire.kalium.logger.KaliumLogLevel +import com.wire.kalium.logger.KaliumLogger +import com.wire.kalium.logic.logStructuredJson +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant +import kotlin.time.Duration + +/** + * Logs the sync process by providing structured logs. + * It logs the sync process start and completion with the syncId as a unique identifier. + */ +internal class SyncManagerLogger( + private val logger: KaliumLogger, + private val syncId: String, + private val syncType: SyncType, + private val syncStartedMoment: Instant +) { + + /** + * Logs the sync process start. + */ + fun logSyncStarted() { + logger.withFeatureId(KaliumLogger.Companion.ApplicationFlow.SYNC).logStructuredJson( + level = KaliumLogLevel.INFO, + leadingMessage = "Started sync process", + jsonStringKeyValues = mapOf( + "syncMetadata" to mapOf( + "id" to syncId, + "status" to SyncStatus.STARTED.name, + "type" to syncType.name + ) + ) + ) + } + + /** + * Logs the sync process completion. + * Optionally, it can pass the duration of the sync process, + * useful for incremental sync that can happen between collecting states. + * + * @param duration optional the duration of the sync process. + */ + fun logSyncCompleted(duration: Duration = Clock.System.now() - syncStartedMoment) { + val logMap = mapOf( + "id" to syncId, + "status" to SyncStatus.COMPLETED.name, + "type" to syncType.name, + "performanceData" to mapOf("timeTakenInMillis" to duration.inWholeMilliseconds) + ) + + logger.withFeatureId(KaliumLogger.Companion.ApplicationFlow.SYNC).logStructuredJson( + level = KaliumLogLevel.INFO, + leadingMessage = "Completed sync process", + jsonStringKeyValues = mapOf("syncMetadata" to logMap) + ) + } +} + +internal enum class SyncStatus { + STARTED, + COMPLETED +} + +internal enum class SyncType { + SLOW, + INCREMENTAL +} + +/** + * Provides a new [SyncManagerLogger] instance with the given parameters. + * @param syncType the [SyncType] that will log. + * @param syncId the unique identifier for the sync process. + * @param syncStartedMoment the moment when the sync process started. + */ +internal fun KaliumLogger.provideNewSyncManagerLogger( + syncType: SyncType, + syncId: String = uuid4().toString(), + syncStartedMoment: Instant = Clock.System.now() +) = SyncManagerLogger(this, syncId, syncType, syncStartedMoment) diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/incremental/IncrementalSyncManager.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/incremental/IncrementalSyncManager.kt index 6e7d2654006..38e2ec5c2cc 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/incremental/IncrementalSyncManager.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/incremental/IncrementalSyncManager.kt @@ -18,6 +18,7 @@ package com.wire.kalium.logic.sync.incremental +import com.benasher44.uuid.uuid4 import com.wire.kalium.logger.KaliumLogger import com.wire.kalium.logger.KaliumLogger.Companion.ApplicationFlow.SYNC import com.wire.kalium.logic.data.event.Event @@ -28,6 +29,8 @@ import com.wire.kalium.logic.data.sync.SlowSyncRepository import com.wire.kalium.logic.data.sync.SlowSyncStatus import com.wire.kalium.logic.kaliumLogger import com.wire.kalium.logic.sync.SyncExceptionHandler +import com.wire.kalium.logic.sync.SyncType +import com.wire.kalium.logic.sync.provideNewSyncManagerLogger import com.wire.kalium.logic.sync.slow.SlowSyncManager import com.wire.kalium.logic.util.ExponentialDurationHelper import com.wire.kalium.logic.util.ExponentialDurationHelperImpl @@ -41,12 +44,15 @@ import kotlinx.coroutines.async import kotlinx.coroutines.cancelChildren import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.cancellable +import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.runningFold import kotlinx.coroutines.launch import kotlinx.coroutines.selects.select +import kotlinx.datetime.Clock import kotlin.time.Duration import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.seconds @@ -181,16 +187,25 @@ internal class IncrementalSyncManager( incrementalSyncWorker .processEventsWhilePolicyAllowsFlow() .cancellable() - .collect { - val newState = when (it) { - EventSource.PENDING -> IncrementalSyncStatus.FetchingPendingEvents + .runningFold(uuid4().toString() to Clock.System.now()) { syncData, eventSource -> + val syncLogger = kaliumLogger.provideNewSyncManagerLogger(SyncType.INCREMENTAL, syncData.first) + val newState = when (eventSource) { + EventSource.PENDING -> { + syncLogger.logSyncStarted() + IncrementalSyncStatus.FetchingPendingEvents + } + EventSource.LIVE -> { + syncLogger.logSyncCompleted(duration = Clock.System.now() - syncData.second) exponentialDurationHelper.reset() IncrementalSyncStatus.Live } } incrementalSyncRepository.updateIncrementalSyncState(newState) - } + + // when the source is LIVE, we need to generate a new syncId since it means the previous one is done + if (eventSource == EventSource.LIVE) uuid4().toString() to Clock.System.now() else syncData + }.collect() incrementalSyncRepository.updateIncrementalSyncState(IncrementalSyncStatus.Pending) logger.i("$TAG IncrementalSync stopped.") } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/slow/SlowSyncManager.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/slow/SlowSyncManager.kt index f04c99e814d..f3c25780c6e 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/slow/SlowSyncManager.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/slow/SlowSyncManager.kt @@ -26,7 +26,9 @@ import com.wire.kalium.logic.data.sync.SlowSyncStatus import com.wire.kalium.logic.functional.combine import com.wire.kalium.logic.kaliumLogger import com.wire.kalium.logic.sync.SyncExceptionHandler +import com.wire.kalium.logic.sync.SyncType import com.wire.kalium.logic.sync.incremental.IncrementalSyncManager +import com.wire.kalium.logic.sync.provideNewSyncManagerLogger import com.wire.kalium.logic.sync.slow.migration.SyncMigrationStepsProvider import com.wire.kalium.logic.sync.slow.migration.steps.SyncMigrationStep import com.wire.kalium.logic.util.ExponentialDurationHelper @@ -187,11 +189,14 @@ internal class SlowSyncManager( } private suspend fun performSlowSync(migrationSteps: List) { + val syncLogger = kaliumLogger.provideNewSyncManagerLogger(SyncType.SLOW) + syncLogger.logSyncStarted() logger.i("Starting SlowSync as all criteria are met and it wasn't performed recently") slowSyncWorker.slowSyncStepsFlow(migrationSteps).cancellable().collect { step -> logger.i("Performing SlowSyncStep $step") slowSyncRepository.updateSlowSyncStatus(SlowSyncStatus.Ongoing(step)) } + syncLogger.logSyncCompleted() logger.i("SlowSync completed. Updating last completion instant") slowSyncRepository.setSlowSyncVersion(CURRENT_VERSION) slowSyncRepository.setLastSlowSyncCompletionInstant(DateTimeUtil.currentInstant()) 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 b086f0f4bfc..8001981da4c 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,7 +19,6 @@ 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 @@ -427,6 +426,8 @@ class ConversationMapperTest { val OTHER_MEMBERS = listOf(ConversationMemberDTO.Other(service = null, id = UserId("other1", "domain1"), conversationRole = "wire_admin")) val MEMBERS_RESPONSE = ConversationMembersResponse(SELF_MEMBER_RESPONSE, OTHER_MEMBERS) + val MESSAGE_DRAFT_ENTITY = MessageDraftEntity(TestConversation.VIEW_ENTITY.id, "text", null, null, listOf()) + val CONVERSATION_RESPONSE = ConversationResponse( "creator", MEMBERS_RESPONSE, 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 b1d9918a4af..70f0807d7d6 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,7 +33,6 @@ import com.wire.kalium.logic.data.id.toApi import com.wire.kalium.logic.data.id.toCrypto import com.wire.kalium.logic.data.id.toDao import com.wire.kalium.logic.data.id.toModel -import com.wire.kalium.logic.data.message.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 @@ -44,6 +43,7 @@ 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 @@ -88,6 +88,7 @@ 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 @@ -99,6 +100,7 @@ import io.mockative.any import io.mockative.coEvery import io.mockative.coVerify import io.mockative.eq +import io.mockative.every import io.mockative.fake.valueOf import io.mockative.matchers.AnyMatcher import io.mockative.matchers.EqualsMatcher @@ -112,6 +114,7 @@ import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest import kotlinx.datetime.Clock import kotlinx.datetime.Instant +import kotlin.test.Ignore import kotlin.test.Test import kotlin.test.assertContains import kotlin.test.assertEquals @@ -698,14 +701,12 @@ class ConversationRepositoryTest { conversationIdEntity, mapOf(UnreadEventTypeEntity.MESSAGE to unreadMessagesCount) ) - val conversationDetailsWithEventsEntity = ConversationDetailsWithEventsEntity( - conversationViewEntity = conversationEntity, - lastMessage = messagePreviewEntity, - unreadEvents = conversationUnreadEventEntity, - ) val (_, conversationRepository) = Arrangement() - .withConversationDetailsWithEvents(listOf(conversationDetailsWithEventsEntity)) + .withConversations(listOf(conversationEntity)) + .withLastMessages(listOf(messagePreviewEntity)) + .withConversationUnreadEvents(listOf(conversationUnreadEventEntity)) + .withMessageDrafts(listOf()) .arrange() // when @@ -715,7 +716,6 @@ class ConversationRepositoryTest { assertContains(result.map { it.conversationDetails.conversation.id }, conversationId) val conversation = result.first { it.conversationDetails.conversation.id == conversationId } - assertIs(conversation.conversationDetails) assertEquals(conversation.unreadEventCount[UnreadEventType.MESSAGE], unreadMessagesCount) assertEquals( MapperProvider.messageMapper(TestUser.SELF.id).fromEntityToMessagePreview(messagePreviewEntity), @@ -733,12 +733,10 @@ 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 @@ -746,14 +744,12 @@ class ConversationRepositoryTest { conversationIdEntity, mapOf(UnreadEventTypeEntity.MESSAGE to unreadMessagesCount) ) - val conversationDetailsWithEventsEntity = ConversationDetailsWithEventsEntity( - conversationViewEntity = conversationEntity, - lastMessage = messagePreviewEntity, - unreadEvents = conversationUnreadEventEntity, - ) val (_, conversationRepository) = Arrangement() - .withConversationDetailsWithEvents(listOf(conversationDetailsWithEventsEntity)) + .withConversations(listOf(conversationEntity)) + .withLastMessages(listOf(MESSAGE_PREVIEW_ENTITY.copy(conversationId = conversationIdEntity))) + .withMessageDrafts(listOf()) + .withConversationUnreadEvents(listOf(conversationUnreadEventEntity)) .arrange() // when @@ -763,7 +759,6 @@ class ConversationRepositoryTest { assertContains(result.map { it.conversationDetails.conversation.id }, conversationId) val conversation = result.first { it.conversationDetails.conversation.id == conversationId } - assertIs(conversation.conversationDetails) assertEquals(conversation.unreadEventCount[UnreadEventType.MESSAGE], unreadMessagesCount) assertEquals(null, conversation.lastMessage) @@ -771,65 +766,63 @@ class ConversationRepositoryTest { } } + // TODO: bring back once pagination is implemented + @Ignore @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() - .withConversationDetailsWithEvents(listOf(conversationDetailsWithEventsEntity)) + .withExpectedObservableConversationDetails(conversationEntity) .arrange() // when - conversationRepository.observeConversationListDetailsWithEvents(shouldFetchFromArchivedConversations).test { + conversationRepository.observeConversationDetailsById(conversationEntity.id.toModel()).test { // then - val conversation = awaitItem().first { it.conversationDetails.conversation.id == conversationId } + val conversationDetail = awaitItem() - assertIs(conversation.conversationDetails) - assertTrue { conversation.lastMessage == null } + assertIs(conversationDetail) +// assertTrue { conversationDetail.lastMessage == null } awaitComplete() } } - @Test - 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"), - ) - val conversationDetailsWithEventsEntity = ConversationDetailsWithEventsEntity(conversationViewEntity = conversationEntity) - - val (_, conversationRepository) = Arrangement() - .withConversationDetailsWithEvents(listOf(conversationDetailsWithEventsEntity)) - .arrange() - - // when - conversationRepository.observeConversationListDetailsWithEvents(shouldFetchFromArchivedConversations).test { - // then - val conversation = awaitItem().first { it.conversationDetails.conversation.id == conversationId } - - assertIs(conversation.conversationDetails) - assertTrue { conversation.lastMessage == null } - - awaitComplete() - } - } + // TODO: bring back once pagination is implemented +// @Test +// 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"), +// ) +// val conversationDetailsWithEventsEntity = ConversationDetailsWithEventsEntity(conversationViewEntity = conversationEntity) +// +// val (_, conversationRepository) = Arrangement() +// .withConversationDetailsWithEvents(listOf(conversationDetailsWithEventsEntity)) +// .arrange() +// +// // when +// conversationRepository.observeConversationListDetailsWithEvents(shouldFetchFromArchivedConversations).test { +// // then +// val conversation = awaitItem().first { it.conversationDetails.conversation.id == conversationId } +// +// assertIs(conversation.conversationDetails) +// assertTrue { conversation.lastMessage == null } +// +// awaitComplete() +// } +// } @Test - fun givenAOneToOneConversationHasNewMessages_whenObservingConversationListDetails_ThenCorrectlyGetUnreadMessageCount() = runTest { + fun givenAGroupConversationHasNewMessages_whenObservingConversationListDetails_ThenCorrectlyGetUnreadMessageCount() = runTest { // given val conversationIdEntity = ConversationIDEntity("some_value", "some_domain") val conversationId = QualifiedID("some_value", "some_domain") @@ -845,13 +838,12 @@ class ConversationRepositoryTest { conversationIdEntity, mapOf(UnreadEventTypeEntity.MESSAGE to unreadMessagesCount) ) - val conversationDetailsWithEventsEntity = ConversationDetailsWithEventsEntity( - conversationViewEntity = conversationEntity, - unreadEvents = conversationUnreadEventEntity, - ) val (_, conversationRepository) = Arrangement() - .withConversationDetailsWithEvents(listOf(conversationDetailsWithEventsEntity)) + .withConversations(listOf(conversationEntity)) + .withLastMessages(listOf()) + .withMessageDrafts(listOf()) + .withConversationUnreadEvents(listOf(conversationUnreadEventEntity)) .arrange() // when @@ -861,37 +853,12 @@ class ConversationRepositoryTest { assertContains(result.map { it.conversationDetails.conversation.id }, conversationId) val conversation = result.first { it.conversationDetails.conversation.id == conversationId } - 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 @@ -1446,9 +1413,16 @@ 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, @@ -1458,9 +1432,10 @@ class ConversationRepositoryTest { memberDAO, conversationApi, messageDAO, + messageDraftDAO, clientDao, clientApi, - conversationMetaDataDAO, + conversationMetaDataDAO ) @@ -1521,22 +1496,40 @@ 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 withConversationDetailsWithEvents(conversations: List) = apply { + suspend fun withLastMessages(messages: List) = apply { coEvery { - conversationDAO.getAllConversationDetailsWithEvents(any(), any(), any()) - }.returns(flowOf(conversations)) + messageDAO.observeLastMessages() + }.returns(flowOf(messages)) + } + + suspend fun withMessageDrafts(messageDrafts: List) = apply { + coEvery { + messageDraftDAO.observeMessageDrafts() + }.returns(flowOf(messageDrafts)) } suspend fun withUpdateConversationReadDateException(exception: Throwable) = apply { @@ -1814,7 +1807,6 @@ 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/data/id/QualifiedIdTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/id/QualifiedIdTest.kt new file mode 100644 index 00000000000..15ddc78f92e --- /dev/null +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/id/QualifiedIdTest.kt @@ -0,0 +1,72 @@ +import com.wire.kalium.logic.data.id.QualifiedID +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + + +class QualifiedIdTest { + @Test + fun givenIdsWithoutDomains_whenEqualsIgnoringBlankDomain_thenReturnsTrue() { + // Given + val qualifiedId1 = QualifiedID("id1", "") + val qualifiedId2 = QualifiedID("id1", "") + + // When + val result = qualifiedId1.equalsIgnoringBlankDomain(qualifiedId2) + + // Then + assertTrue(result) + } + + @Test + fun givenOneIdWithoutDomain_whenEqualsIgnoringBlankDomain_thenReturnsTrue() { + // Given + val qualifiedId1 = QualifiedID("id1", "") + val qualifiedId2 = QualifiedID("id1", "domain") + + // When + val result = qualifiedId1.equalsIgnoringBlankDomain(qualifiedId2) + + // Then + assertTrue(result) + } + + @Test + fun givenIdsWithSameDomains_whenEqualsIgnoringBlankDomain_thenReturnsTrue() { + // Given + val qualifiedId1 = QualifiedID("id1", "domain") + val qualifiedId2 = QualifiedID("id1", "domain") + + // When + val result = qualifiedId1.equalsIgnoringBlankDomain(qualifiedId2) + + // Then + assertTrue(result) + } + + @Test + fun givenIdsWithDifferentDomains_whenEqualsIgnoringBlankDomain_thenReturnsFalse() { + // Given + val qualifiedId1 = QualifiedID("id1", "domain1") + val qualifiedId2 = QualifiedID("id1", "domain2") + + // When + val result = qualifiedId1.equalsIgnoringBlankDomain(qualifiedId2) + + // Then + assertTrue(!result) + } + + @Test + fun givenIdsWithDifferentValues_whenEqualsIgnoringBlankDomain_thenReturnsFalse() { + // Given + val qualifiedId1 = QualifiedID("id1", "") + val qualifiedId2 = QualifiedID("id2", "") + + // When + val result = qualifiedId1.equalsIgnoringBlankDomain(qualifiedId2) + + // Then + assertFalse(result) + } +} \ No newline at end of file diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/user/UploadUserAvatarUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/user/UploadUserAvatarUseCaseTest.kt index 943cac79327..02108d9c82b 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/user/UploadUserAvatarUseCaseTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/user/UploadUserAvatarUseCaseTest.kt @@ -21,15 +21,9 @@ package com.wire.kalium.logic.feature.user import com.wire.kalium.logic.CoreFailure import com.wire.kalium.logic.data.asset.AssetRepository import com.wire.kalium.logic.data.asset.UploadedAssetId -import com.wire.kalium.logic.data.user.ConnectionState -import com.wire.kalium.logic.data.user.SelfUser -import com.wire.kalium.logic.data.user.SupportedProtocol -import com.wire.kalium.logic.data.user.UserAssetId -import com.wire.kalium.logic.data.user.UserAvailabilityStatus -import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.data.user.UserRepository -import com.wire.kalium.logic.data.user.type.UserType import com.wire.kalium.logic.functional.Either +import com.wire.kalium.util.KaliumDispatcherImpl import io.mockative.Mock import io.mockative.any import io.mockative.coEvery @@ -106,29 +100,15 @@ class UploadUserAvatarUseCaseTest { @Mock val userRepository = mock(UserRepository::class) - private val uploadUserAvatarUseCase: UploadUserAvatarUseCase = UploadUserAvatarUseCaseImpl(userRepository, assetRepository) + val dispatcher = KaliumDispatcherImpl + + private val uploadUserAvatarUseCase: UploadUserAvatarUseCase = + UploadUserAvatarUseCaseImpl(userRepository, assetRepository, dispatcher) var userHomePath = "/Users/me".toPath() val fakeFileSystem = FakeFileSystem().also { it.createDirectories(userHomePath) } - private val dummySelfUser = SelfUser( - id = UserId("some_id", "some_domain"), - name = "some_name", - handle = "some_handle", - email = "some_email", - phone = null, - accentId = 1, - teamId = null, - connectionStatus = ConnectionState.ACCEPTED, - previewPicture = UserAssetId("value1", "domain"), - completePicture = UserAssetId("value2", "domain"), - userType = UserType.INTERNAL, - availabilityStatus = UserAvailabilityStatus.NONE, - expiresAt = null, - supportedProtocols = setOf(SupportedProtocol.PROTEUS) - ) - fun withStoredData(data: ByteArray, dataNamePath: Path): Arrangement { val fullDataPath = "$userHomePath/$dataNamePath".toPath() fakeFileSystem.write(fullDataPath) { diff --git a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/ConversationDetails.sq b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/ConversationDetails.sq new file mode 100644 index 00000000000..88746b79ac5 --- /dev/null +++ b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/ConversationDetails.sq @@ -0,0 +1,147 @@ +CREATE VIEW IF NOT EXISTS ConversationDetails AS +SELECT +Conversation.qualified_id AS qualifiedId, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.name + WHEN 'CONNECTION_PENDING' THEN connection_user.name + ELSE Conversation.name +END AS name, +Conversation.type, +Call.status AS callStatus, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.preview_asset_id + WHEN 'CONNECTION_PENDING' THEN connection_user.preview_asset_id +END AS previewAssetId, +Conversation.muted_status AS mutedStatus, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.team + ELSE Conversation.team_id +END AS teamId, +CASE (Conversation.type) + WHEN 'CONNECTION_PENDING' THEN Connection.last_update_date + ELSE Conversation.last_modified_date +END AS lastModifiedDate, +Conversation.last_read_date AS lastReadDate, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.user_availability_status + WHEN 'CONNECTION_PENDING' THEN connection_user.user_availability_status +END AS userAvailabilityStatus, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.user_type + WHEN 'CONNECTION_PENDING' THEN connection_user.user_type +END AS userType, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.bot_service + WHEN 'CONNECTION_PENDING' THEN connection_user.bot_service +END AS botService, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.deleted + WHEN 'CONNECTION_PENDING' THEN connection_user.deleted +END AS userDeleted, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.defederated + WHEN 'CONNECTION_PENDING' THEN connection_user.defederated +END AS userDefederated, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.supported_protocols + WHEN 'CONNECTION_PENDING' THEN connection_user.supported_protocols +END AS userSupportedProtocols, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.connection_status + WHEN 'CONNECTION_PENDING' THEN connection_user.connection_status +END AS connectionStatus, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.qualified_id + WHEN 'CONNECTION_PENDING' THEN connection_user.qualified_id +END AS otherUserId, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.active_one_on_one_conversation_id + WHEN 'CONNECTION_PENDING' THEN connection_user.active_one_on_one_conversation_id +END AS otherUserActiveConversationId, +CASE + WHEN (SelfUser.id LIKE (Conversation.creator_id || '@%')) THEN 1 + ELSE 0 +END AS isCreator, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN coalesce(User.active_one_on_one_conversation_id = Conversation.qualified_id, 0) + ELSE 1 +END AS isActive, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.accent_id + ELSE 0 +END AS accentId, +Conversation.last_notified_date AS lastNotifiedMessageDate, +memberRole. role AS selfRole, +Conversation.protocol, +Conversation.mls_cipher_suite, +Conversation.mls_epoch, +Conversation.mls_group_id, +Conversation.mls_last_keying_material_update_date, +Conversation.mls_group_state, +Conversation.access_list, +Conversation.access_role_list, +Conversation.mls_proposal_timer, +Conversation.muted_time, +Conversation.creator_id, +Conversation.receipt_mode, +Conversation.message_timer, +Conversation.user_message_timer, +Conversation.incomplete_metadata, +Conversation.archived, +Conversation.archived_date_time, +Conversation.verification_status AS mls_verification_status, +Conversation.proteus_verification_status, +Conversation.legal_hold_status, +SelfUser.id AS selfUserId, +CASE + WHEN Conversation.type = 'GROUP' THEN + CASE + WHEN memberRole.role IS NOT NULL THEN 1 + ELSE 0 + END + WHEN Conversation.type = 'ONE_ON_ONE' THEN + CASE + WHEN User.defederated = 1 THEN 0 + WHEN User.deleted = 1 THEN 0 + WHEN User.connection_status = 'BLOCKED' THEN 0 + WHEN Conversation.legal_hold_status = 'DEGRADED' THEN 0 + ELSE 1 + END + ELSE 0 +END AS interactionEnabled +FROM Conversation +LEFT JOIN SelfUser +LEFT JOIN Member ON Conversation.qualified_id = Member.conversation + AND Conversation.type IS 'ONE_ON_ONE' + AND Member.user IS NOT SelfUser.id +LEFT JOIN Member AS memberRole ON Conversation.qualified_id = memberRole.conversation + AND memberRole.user IS SelfUser.id +LEFT JOIN User ON User.qualified_id = Member.user +LEFT JOIN Connection ON Connection.qualified_conversation = Conversation.qualified_id + AND (Connection.status = 'SENT' + OR Connection.status = 'PENDING' + OR Connection.status = 'NOT_CONNECTED' + AND Conversation.type IS 'CONNECTION_PENDING') +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); + +selectAllConversationDetails: +SELECT * FROM ConversationDetails +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 archived = :fromArchive + AND isActive +ORDER BY lastModifiedDate DESC, name IS NULL, name COLLATE NOCASE ASC; + +selectConversationDetailsByQualifiedId: +SELECT * FROM ConversationDetails WHERE qualifiedId = ?; + +selectConversationDetailsByGroupId: +SELECT * FROM ConversationDetails WHERE mls_group_id = ?; diff --git a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/ConversationDetailsWithEvents.sq b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/ConversationDetailsWithEvents.sq index f160336dc89..4f7328188af 100644 --- a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/ConversationDetailsWithEvents.sq +++ b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/ConversationDetailsWithEvents.sq @@ -88,10 +88,20 @@ ORDER BY name COLLATE NOCASE ASC; selectConversationDetailsWithEvents: - SELECT * FROM ConversationDetailsWithEvents - WHERE - archived = :fromArchive - AND CASE WHEN :onlyInteractionsEnabled THEN interactionEnabled = 1 ELSE 1 END +SELECT * FROM ConversationDetailsWithEvents +WHERE + archived = :fromArchive + AND CASE + -- When filter is ALL, do not apply additional filters on conversation type + WHEN :conversationFilter = 'ALL' THEN 1 = 1 + -- When filter is GROUPS, filter only group conversations + WHEN :conversationFilter = 'GROUPS' THEN type = 'GROUP' + -- When filter is ONE_ON_ONE, filter only one-on-one conversations + WHEN :conversationFilter = 'ONE_ON_ONE' THEN type = 'ONE_ON_ONE' + -- When filter is FAVORITES (future implementation) + ELSE 1 = 0 + END + AND CASE WHEN :onlyInteractionsEnabled THEN interactionEnabled = 1 ELSE 1 END ORDER BY CASE WHEN :newActivitiesOnTop THEN hasNewActivitiesToShow ELSE 0 END DESC, lastModifiedDate DESC, @@ -104,6 +114,16 @@ selectConversationDetailsWithEventsFromSearch: SELECT * FROM ConversationDetailsWithEvents WHERE archived = :fromArchive + AND CASE + -- When filter is ALL, do not apply additional filters on conversation type + WHEN :conversationFilter = 'ALL' THEN 1 = 1 + -- When filter is GROUPS, filter only group conversations + WHEN :conversationFilter = 'GROUPS' THEN type = 'GROUP' + -- When filter is ONE_ON_ONE, filter only one-on-one conversations + WHEN :conversationFilter = 'ONE_ON_ONE' THEN type = 'ONE_ON_ONE' + -- When filter is FAVORITES (future implementation) + ELSE 1 = 0 + END AND CASE WHEN :onlyInteractionsEnabled THEN interactionEnabled = 1 ELSE 1 END AND name LIKE ('%' || :searchQuery || '%') ORDER BY @@ -127,6 +147,16 @@ WHERE AND (ConversationDetails.protocol IS 'PROTEUS' OR ConversationDetails.protocol IS 'MIXED' OR (ConversationDetails.protocol IS 'MLS' AND ConversationDetails.mls_group_state IS 'ESTABLISHED')) AND ConversationDetails.isActive AND archived = :fromArchive + AND CASE + -- When filter is ALL, do not apply additional filters on conversation type + WHEN :conversationFilter = 'ALL' THEN 1 = 1 + -- When filter is GROUPS, filter only group conversations + WHEN :conversationFilter = 'GROUPS' THEN type = 'GROUP' + -- When filter is ONE_ON_ONE, filter only one-on-one conversations + WHEN :conversationFilter = 'ONE_ON_ONE' THEN type = 'ONE_ON_ONE' + -- When filter is FAVORITES (future implementation) + ELSE 1 = 0 + END AND CASE WHEN :onlyInteractionsEnabled THEN interactionEnabled = 1 ELSE 1 END; countConversationDetailsWithEventsFromSearch: @@ -142,5 +172,15 @@ WHERE AND (ConversationDetails.protocol IS 'PROTEUS' OR ConversationDetails.protocol IS 'MIXED' OR (ConversationDetails.protocol IS 'MLS' AND ConversationDetails.mls_group_state IS 'ESTABLISHED')) AND ConversationDetails.isActive AND archived = :fromArchive + AND CASE + -- When filter is ALL, do not apply additional filters on conversation type + WHEN :conversationFilter = 'ALL' THEN 1 = 1 + -- When filter is GROUPS, filter only group conversations + WHEN :conversationFilter = 'GROUPS' THEN type = 'GROUP' + -- When filter is ONE_ON_ONE, filter only one-on-one conversations + WHEN :conversationFilter = 'ONE_ON_ONE' THEN type = 'ONE_ON_ONE' + -- When filter is FAVORITES (future implementation) + ELSE 1 = 0 + END AND CASE WHEN :onlyInteractionsEnabled THEN interactionEnabled = 1 ELSE 1 END AND name LIKE ('%' || :searchQuery || '%'); 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 d856bcd4851..62042685973 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 @@ -167,256 +167,6 @@ UPDATE Conversation SET degraded_conversation_notified = ? WHERE qualified_id = :qualified_id; -CREATE VIEW IF NOT EXISTS ConversationDetails AS -SELECT -Conversation.qualified_id AS qualifiedId, -CASE (Conversation.type) - WHEN 'ONE_ON_ONE' THEN User.name - WHEN 'CONNECTION_PENDING' THEN connection_user.name - ELSE Conversation.name -END AS name, -Conversation.type, -Call.status AS callStatus, -CASE (Conversation.type) - WHEN 'ONE_ON_ONE' THEN User.preview_asset_id - WHEN 'CONNECTION_PENDING' THEN connection_user.preview_asset_id -END AS previewAssetId, -Conversation.muted_status AS mutedStatus, -CASE (Conversation.type) - WHEN 'ONE_ON_ONE' THEN User.team - ELSE Conversation.team_id -END AS teamId, -CASE (Conversation.type) - WHEN 'CONNECTION_PENDING' THEN Connection.last_update_date - ELSE Conversation.last_modified_date -END AS lastModifiedDate, -Conversation.last_read_date AS lastReadDate, -CASE (Conversation.type) - WHEN 'ONE_ON_ONE' THEN User.user_availability_status - WHEN 'CONNECTION_PENDING' THEN connection_user.user_availability_status -END AS userAvailabilityStatus, -CASE (Conversation.type) - WHEN 'ONE_ON_ONE' THEN User.user_type - WHEN 'CONNECTION_PENDING' THEN connection_user.user_type -END AS userType, -CASE (Conversation.type) - WHEN 'ONE_ON_ONE' THEN User.bot_service - WHEN 'CONNECTION_PENDING' THEN connection_user.bot_service -END AS botService, -CASE (Conversation.type) - WHEN 'ONE_ON_ONE' THEN User.deleted - WHEN 'CONNECTION_PENDING' THEN connection_user.deleted -END AS userDeleted, -CASE (Conversation.type) - WHEN 'ONE_ON_ONE' THEN User.defederated - WHEN 'CONNECTION_PENDING' THEN connection_user.defederated -END AS userDefederated, -CASE (Conversation.type) - WHEN 'ONE_ON_ONE' THEN User.supported_protocols - WHEN 'CONNECTION_PENDING' THEN connection_user.supported_protocols -END AS userSupportedProtocols, -CASE (Conversation.type) - WHEN 'ONE_ON_ONE' THEN User.connection_status - WHEN 'CONNECTION_PENDING' THEN connection_user.connection_status -END AS connectionStatus, -CASE (Conversation.type) - WHEN 'ONE_ON_ONE' THEN User.qualified_id - WHEN 'CONNECTION_PENDING' THEN connection_user.qualified_id -END AS otherUserId, -CASE (Conversation.type) - WHEN 'ONE_ON_ONE' THEN User.active_one_on_one_conversation_id - WHEN 'CONNECTION_PENDING' THEN connection_user.active_one_on_one_conversation_id -END AS otherUserActiveConversationId, -CASE - WHEN ((SELECT id FROM SelfUser LIMIT 1) LIKE (Conversation.creator_id || '@%')) THEN 1 - ELSE 0 -END AS isCreator, -CASE (Conversation.type) - WHEN 'ONE_ON_ONE' THEN coalesce(User.active_one_on_one_conversation_id = Conversation.qualified_id, 0) - ELSE 1 -END AS isActive, -CASE (Conversation.type) - WHEN 'ONE_ON_ONE' THEN User.accent_id - ELSE 0 -END AS accentId, -Conversation.last_notified_date AS lastNotifiedMessageDate, -memberRole. role AS selfRole, -Conversation.protocol, -Conversation.mls_cipher_suite, -Conversation.mls_epoch, -Conversation.mls_group_id, -Conversation.mls_last_keying_material_update_date, -Conversation.mls_group_state, -Conversation.access_list, -Conversation.access_role_list, -Conversation.team_id, -Conversation.mls_proposal_timer, -Conversation.muted_time, -Conversation.creator_id, -Conversation.last_modified_date, -Conversation.receipt_mode, -Conversation.message_timer, -Conversation.user_message_timer, -Conversation.incomplete_metadata, -Conversation.archived, -Conversation.archived_date_time, -Conversation.verification_status AS mls_verification_status, -Conversation.proteus_verification_status, -Conversation.legal_hold_status -FROM Conversation -LEFT JOIN Member ON Conversation.qualified_id = Member.conversation - AND Conversation.type IS 'ONE_ON_ONE' - AND Member.user IS NOT (SELECT SelfUser.id FROM SelfUser LIMIT 1) -LEFT JOIN Member AS memberRole ON Conversation.qualified_id = memberRole.conversation - AND memberRole.user IS (SELECT SelfUser.id FROM SelfUser LIMIT 1) -LEFT JOIN User ON User.qualified_id = Member.user -LEFT JOIN Connection ON Connection.qualified_conversation = Conversation.qualified_id - AND (Connection.status = 'SENT' - OR Connection.status = 'PENDING' - OR Connection.status = 'NOT_CONNECTED' - AND Conversation.type IS 'CONNECTION_PENDING') -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 filter is ALL, do not apply additional filters on conversation type - WHEN :conversationFilter = 'ALL' THEN 1 = 1 - -- When filter is GROUPS, filter only group conversations - WHEN :conversationFilter = 'GROUPS' THEN type = 'GROUP' - -- When filter is ONE_ON_ONE, filter only one-on-one conversations - WHEN :conversationFilter = 'ONE_ON_ONE' THEN type = 'ONE_ON_ONE' - -- When filter is FAVORITES (future implementation) - ELSE 1 = 0 - END - 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 - 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 archived = :fromArchive - AND isActive -ORDER BY lastModifiedDate DESC, name IS NULL, name COLLATE NOCASE ASC; - selectAllConversations: SELECT * FROM Conversation WHERE type IS NOT 'CONNECTION_PENDING' ORDER BY last_modified_date DESC, name ASC; @@ -429,7 +179,7 @@ FROM Conversation WHERE type IS 'GROUP' AND protocol IS 'MIXED' AND team_id = ? AND memberCount = mlsCapableMemberCount; selectByQualifiedId: -SELECT * FROM ConversationDetails WHERE qualifiedId = ?; +SELECT * FROM Conversation WHERE qualified_id = ?; selectConversationByQualifiedId: SELECT @@ -441,10 +191,10 @@ SELECT protocol, mls_group_id, mls_group_state, mls_epoch , mls_last_keying_material_update_date, mls_cipher_suite FROM Conversation WHERE qualified_id = ?; selectReceiptModeFromGroupConversationByQualifiedId: -SELECT receipt_mode FROM ConversationDetails WHERE qualifiedId = ? AND type IS 'GROUP'; +SELECT receipt_mode FROM Conversation WHERE qualified_id = ? AND type IS 'GROUP'; selectByGroupId: -SELECT * FROM ConversationDetails WHERE mls_group_id = ?; +SELECT * FROM Conversation WHERE mls_group_id = ?; selectByGroupState: SELECT * FROM Conversation WHERE mls_group_state = ? AND (protocol IS 'MLS' OR protocol IS 'MIXED'); 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 68c2bff623e..f57c6006a50 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,41 +45,13 @@ 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; -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; +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) +); diff --git a/persistence/src/commonMain/db_user/migrations/88.sqm b/persistence/src/commonMain/db_user/migrations/88.sqm new file mode 100644 index 00000000000..c7675b06eb5 --- /dev/null +++ b/persistence/src/commonMain/db_user/migrations/88.sqm @@ -0,0 +1,215 @@ +DROP VIEW IF EXISTS LastMessagePreview; +DROP VIEW IF EXISTS ConversationDetails; +DROP VIEW IF EXISTS ConversationDetailsWithEvents; + +CREATE VIEW IF NOT EXISTS ConversationDetails AS +SELECT +Conversation.qualified_id AS qualifiedId, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.name + WHEN 'CONNECTION_PENDING' THEN connection_user.name + ELSE Conversation.name +END AS name, +Conversation.type, +Call.status AS callStatus, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.preview_asset_id + WHEN 'CONNECTION_PENDING' THEN connection_user.preview_asset_id +END AS previewAssetId, +Conversation.muted_status AS mutedStatus, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.team + ELSE Conversation.team_id +END AS teamId, +CASE (Conversation.type) + WHEN 'CONNECTION_PENDING' THEN Connection.last_update_date + ELSE Conversation.last_modified_date +END AS lastModifiedDate, +Conversation.last_read_date AS lastReadDate, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.user_availability_status + WHEN 'CONNECTION_PENDING' THEN connection_user.user_availability_status +END AS userAvailabilityStatus, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.user_type + WHEN 'CONNECTION_PENDING' THEN connection_user.user_type +END AS userType, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.bot_service + WHEN 'CONNECTION_PENDING' THEN connection_user.bot_service +END AS botService, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.deleted + WHEN 'CONNECTION_PENDING' THEN connection_user.deleted +END AS userDeleted, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.defederated + WHEN 'CONNECTION_PENDING' THEN connection_user.defederated +END AS userDefederated, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.supported_protocols + WHEN 'CONNECTION_PENDING' THEN connection_user.supported_protocols +END AS userSupportedProtocols, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.connection_status + WHEN 'CONNECTION_PENDING' THEN connection_user.connection_status +END AS connectionStatus, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.qualified_id + WHEN 'CONNECTION_PENDING' THEN connection_user.qualified_id +END AS otherUserId, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.active_one_on_one_conversation_id + WHEN 'CONNECTION_PENDING' THEN connection_user.active_one_on_one_conversation_id +END AS otherUserActiveConversationId, +CASE + WHEN (SelfUser.id LIKE (Conversation.creator_id || '@%')) THEN 1 + ELSE 0 +END AS isCreator, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN coalesce(User.active_one_on_one_conversation_id = Conversation.qualified_id, 0) + ELSE 1 +END AS isActive, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.accent_id + ELSE 0 +END AS accentId, +Conversation.last_notified_date AS lastNotifiedMessageDate, +memberRole. role AS selfRole, +Conversation.protocol, +Conversation.mls_cipher_suite, +Conversation.mls_epoch, +Conversation.mls_group_id, +Conversation.mls_last_keying_material_update_date, +Conversation.mls_group_state, +Conversation.access_list, +Conversation.access_role_list, +Conversation.mls_proposal_timer, +Conversation.muted_time, +Conversation.creator_id, +Conversation.receipt_mode, +Conversation.message_timer, +Conversation.user_message_timer, +Conversation.incomplete_metadata, +Conversation.archived, +Conversation.archived_date_time, +Conversation.verification_status AS mls_verification_status, +Conversation.proteus_verification_status, +Conversation.legal_hold_status, +SelfUser.id AS selfUserId, +CASE + WHEN Conversation.type = 'GROUP' THEN + CASE + WHEN memberRole.role IS NOT NULL THEN 1 + ELSE 0 + END + WHEN Conversation.type = 'ONE_ON_ONE' THEN + CASE + WHEN User.defederated = 1 THEN 0 + WHEN User.deleted = 1 THEN 0 + WHEN User.connection_status = 'BLOCKED' THEN 0 + WHEN Conversation.legal_hold_status = 'DEGRADED' THEN 0 + ELSE 1 + END + ELSE 0 +END AS interactionEnabled +FROM Conversation +LEFT JOIN SelfUser +LEFT JOIN Member ON Conversation.qualified_id = Member.conversation + AND Conversation.type IS 'ONE_ON_ONE' + AND Member.user IS NOT SelfUser.id +LEFT JOIN Member AS memberRole ON Conversation.qualified_id = memberRole.conversation + AND memberRole.user IS SelfUser.id +LEFT JOIN User ON User.qualified_id = Member.user +LEFT JOIN Connection ON Connection.qualified_conversation = Conversation.qualified_id + AND (Connection.status = 'SENT' + OR Connection.status = 'PENDING' + OR Connection.status = 'NOT_CONNECTED' + AND Conversation.type IS 'CONNECTION_PENDING') +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.*, + -- unread events + SUM(CASE WHEN UnreadEvent.type = 'KNOCK' THEN 1 ELSE 0 END) AS unreadKnocksCount, + SUM(CASE WHEN UnreadEvent.type = 'MISSED_CALL' THEN 1 ELSE 0 END) AS unreadMissedCallsCount, + SUM(CASE WHEN UnreadEvent.type = 'MENTION' THEN 1 ELSE 0 END) AS unreadMentionsCount, + SUM(CASE WHEN UnreadEvent.type = 'REPLY' THEN 1 ELSE 0 END) AS unreadRepliesCount, + SUM(CASE WHEN UnreadEvent.type = 'MESSAGE' THEN 1 ELSE 0 END) 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 COUNT(UnreadEvent.id) > 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 SUM(CASE WHEN UnreadEvent.type IN ('MENTION', 'REPLY') THEN 1 ELSE 0 END) > 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, + -- draft message + MessageDraft.text AS messageDraftText, + MessageDraft.edit_message_id AS messageDraftEditMessageId, + MessageDraft.quoted_message_id AS messageDraftQuotedMessageId, + MessageDraft.mention_list AS messageDraftMentionList, + -- last message + LastMessage.id AS lastMessageId, + LastMessage.content_type AS lastMessageContentType, + LastMessage.creation_date AS lastMessageDate, + LastMessage.visibility AS lastMessageVisibility, + LastMessage.sender_user_id AS lastMessageSenderUserId, + (LastMessage.expire_after_millis IS NOT NULL) AS lastMessageIsEphemeral, + User.name AS lastMessageSenderName, + User.connection_status AS lastMessageSenderConnectionStatus, + User.deleted AS lastMessageSenderIsDeleted, + (LastMessage.sender_user_id IS NOT NULL AND LastMessage.sender_user_id == ConversationDetails.selfUserId) AS lastMessageIsSelfMessage, + MemberChangeContent.member_change_list AS lastMessageMemberChangeList, + MemberChangeContent.member_change_type AS lastMessageMemberChangeType, + ConversationNameChangedContent.conversation_name AS lastMessageUpdateConversationName, + (Mention.user_id IS NOT NULL) AS lastMessageIsMentioningSelfUser, + TextContent.is_quoting_self AS lastMessageIsQuotingSelfUser, + TextContent.text_body AS lastMessageText, + AssetContent.asset_mime_type AS lastMessageAssetMimeType +FROM ConversationDetails +LEFT JOIN UnreadEvent + ON UnreadEvent.conversation_id = ConversationDetails.qualifiedId +LEFT JOIN MessageDraft + ON ConversationDetails.qualifiedId = MessageDraft.conversation_id AND ConversationDetails.archived = 0 -- only return message draft for non-archived conversations +LEFT JOIN Message AS LastMessage + ON LastMessage.id = ( + SELECT Message.id + FROM Message + WHERE ConversationDetails.qualifiedId = Message.conversation_id + ORDER BY Message.creation_date DESC + LIMIT 1 + ) AND ConversationDetails.archived = 0 -- only return last message for non-archived conversations +LEFT JOIN User + ON LastMessage.sender_user_id = User.qualified_id +LEFT JOIN MessageMemberChangeContent AS MemberChangeContent + ON LastMessage.id = MemberChangeContent.message_id AND LastMessage.conversation_id = MemberChangeContent.conversation_id +LEFT JOIN MessageMention AS Mention + ON LastMessage.id == Mention.message_id AND ConversationDetails.selfUserId == Mention.user_id +LEFT JOIN MessageConversationChangedContent AS ConversationNameChangedContent + ON LastMessage.id = ConversationNameChangedContent.message_id AND LastMessage.conversation_id = ConversationNameChangedContent.conversation_id +LEFT JOIN MessageAssetContent AS AssetContent + ON LastMessage.id = AssetContent.message_id AND LastMessage.conversation_id = AssetContent.conversation_id +LEFT JOIN MessageTextContent AS TextContent + ON LastMessage.id = TextContent.message_id AND LastMessage.conversation_id = TextContent.conversation_id +WHERE + ConversationDetails.type IS NOT 'SELF' + AND ( + ConversationDetails.type IS 'GROUP' + OR (ConversationDetails.type IS 'ONE_ON_ONE' AND (ConversationDetails.name IS NOT NULL AND ConversationDetails.otherUserId IS NOT NULL)) -- show 1:1 convos if they have user metadata + OR (ConversationDetails.type IS 'ONE_ON_ONE' AND ConversationDetails.userDeleted = 1) -- show deleted 1:1 convos to maintain prev, logic + OR (ConversationDetails.type IS 'CONNECTION_PENDING' AND ConversationDetails.otherUserId IS NOT NULL) -- show connection requests even without metadata + ) + AND (ConversationDetails.protocol IS 'PROTEUS' OR ConversationDetails.protocol IS 'MIXED' OR (ConversationDetails.protocol IS 'MLS' AND ConversationDetails.mls_group_state IS 'ESTABLISHED')) + AND ConversationDetails.isActive +GROUP BY ConversationDetails.qualifiedId; 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 084dd6a8617..dc815aa8c3d 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 @@ -78,7 +78,7 @@ interface ConversationDAO { suspend fun observeOneOnOneConversationWithOtherUser(userId: UserIDEntity): Flow suspend fun getConversationProtocolInfo(qualifiedID: QualifiedIDEntity): ConversationEntity.ProtocolInfo? - suspend fun observeConversationByGroupID(groupID: String): Flow + suspend fun observeConversationDetailsByGroupID(groupID: String): Flow suspend fun getConversationIdByGroupID(groupID: String): QualifiedIDEntity? suspend fun getConversationsByGroupState(groupState: ConversationEntity.GroupState): List suspend fun deleteConversationByQualifiedID(qualifiedID: QualifiedIDEntity) @@ -132,7 +132,7 @@ interface ConversationDAO { suspend fun getConversationsWithoutMetadata(): List suspend fun clearContent(conversationId: QualifiedIDEntity) suspend fun updateMlsVerificationStatus(verificationStatus: ConversationEntity.VerificationStatus, conversationId: QualifiedIDEntity) - suspend fun getConversationByGroupID(groupID: String): ConversationViewEntity? + suspend fun getConversationDetailsByGroupID(groupID: String): ConversationViewEntity? suspend fun observeUnreadArchivedConversationsCount(): Flow suspend fun observeDegradedConversationNotified(conversationId: QualifiedIDEntity): Flow suspend fun updateDegradedConversationNotifiedFlag(conversationId: QualifiedIDEntity, updateFlag: Boolean) 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 e75b781aec0..d184b79bacb 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 @@ -20,6 +20,8 @@ package com.wire.kalium.persistence.dao.conversation import app.cash.sqldelight.coroutines.asFlow import com.wire.kalium.persistence.ConversationsQueries +import com.wire.kalium.persistence.ConversationDetailsQueries +import com.wire.kalium.persistence.ConversationDetailsWithEventsQueries import com.wire.kalium.persistence.MembersQueries import com.wire.kalium.persistence.UnreadEventsQueries import com.wire.kalium.persistence.cache.FlowCache @@ -49,11 +51,13 @@ internal val MLS_DEFAULT_CIPHER_SUITE = ConversationEntity.CipherSuite.MLS_128_D // TODO: Refactor. We can split this into smaller DAOs. // For example, one for Members, one for Protocol/MLS-related things, etc. // Even if they operate on the same table underneath, these DAOs can represent/do different things. -@Suppress("TooManyFunctions") +@Suppress("TooManyFunctions", "LongParameterList") internal class ConversationDAOImpl internal constructor( private val conversationDetailsCache: FlowCache, private val conversationCache: FlowCache, private val conversationQueries: ConversationsQueries, + private val conversationDetailsQueries: ConversationDetailsQueries, + private val conversationDetailsWithEventsQueries: ConversationDetailsWithEventsQueries, private val memberQueries: MembersQueries, private val unreadEventsQueries: UnreadEventsQueries, private val coroutineContext: CoroutineContext, @@ -61,7 +65,7 @@ internal class ConversationDAOImpl internal constructor( private val conversationMapper = ConversationMapper private val conversationDetailsWithEventsMapper = ConversationDetailsWithEventsMapper override val platformExtensions: ConversationExtensions = - ConversationExtensionsImpl(conversationQueries, conversationDetailsWithEventsMapper, coroutineContext) + ConversationExtensionsImpl(conversationDetailsWithEventsQueries, conversationDetailsWithEventsMapper, coroutineContext) // region Get/Observe by ID @@ -82,7 +86,7 @@ internal class ConversationDAOImpl internal constructor( override suspend fun observeConversationDetailsById( conversationId: QualifiedIDEntity ): Flow = conversationDetailsCache.get(conversationId) { - conversationQueries.selectByQualifiedId(conversationId, conversationMapper::fromViewToModel) + conversationDetailsQueries.selectConversationDetailsByQualifiedId(conversationId, conversationMapper::fromViewToModel) .asFlow() .mapToOneOrNull() } @@ -211,7 +215,7 @@ internal class ConversationDAOImpl internal constructor( } override suspend fun getAllConversationDetails(fromArchive: Boolean): Flow> { - return conversationQueries.selectAllConversationDetails(fromArchive, conversationMapper::fromViewToModel) + return conversationDetailsQueries.selectAllConversationDetails(fromArchive, conversationMapper::fromViewToModel) .asFlow() .mapToList() .flowOn(coroutineContext) @@ -222,7 +226,7 @@ internal class ConversationDAOImpl internal constructor( onlyInteractionEnabled: Boolean, newActivitiesOnTop: Boolean, ): Flow> { - return conversationQueries.selectAllConversationDetailsWithEvents( + return conversationDetailsWithEventsQueries.selectAllConversationDetailsWithEvents( fromArchive = fromArchive, onlyInteractionsEnabled = onlyInteractionEnabled, newActivitiesOnTop = newActivitiesOnTop, @@ -275,15 +279,15 @@ internal class ConversationDAOImpl internal constructor( conversationQueries.selectProtocolInfoByQualifiedId(qualifiedID, conversationMapper::mapProtocolInfo).executeAsOneOrNull() } - override suspend fun observeConversationByGroupID(groupID: String): Flow { - return conversationQueries.selectByGroupId(groupID, conversationMapper::fromViewToModel) + override suspend fun observeConversationDetailsByGroupID(groupID: String): Flow { + return conversationDetailsQueries.selectConversationDetailsByGroupId(groupID, conversationMapper::fromViewToModel) .asFlow() .flowOn(coroutineContext) .mapToOneOrNull() } - override suspend fun getConversationByGroupID(groupID: String): ConversationViewEntity? { - return conversationQueries.selectByGroupId(groupID, conversationMapper::fromViewToModel) + override suspend fun getConversationDetailsByGroupID(groupID: String): ConversationViewEntity? { + return conversationDetailsQueries.selectConversationDetailsByGroupId(groupID, conversationMapper::fromViewToModel) .executeAsOneOrNull() } 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 599a41b2f96..4f76e77ffad 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 @@ -66,11 +66,9 @@ data object ConversationDetailsWithEventsMapper { mlsGroupState: ConversationEntity.GroupState, accessList: List, accessRoleList: List, - unusedTeamId: String?, mlsProposalTimer: String?, mutedTime: Long, creatorId: String, - unusedLastModifiedDate: Instant, receiptMode: ConversationEntity.ReceiptMode, messageTimer: Long?, userMessageTimer: Long?, @@ -80,6 +78,7 @@ data object ConversationDetailsWithEventsMapper { mlsVerificationStatus: ConversationEntity.VerificationStatus, proteusVerificationStatus: ConversationEntity.VerificationStatus, legalHoldStatus: ConversationEntity.LegalHoldStatus, + selfUserId: QualifiedIDEntity?, interactionEnabled: Long, unreadKnocksCount: Long?, unreadMissedCallsCount: Long?, @@ -87,6 +86,10 @@ data object ConversationDetailsWithEventsMapper { unreadRepliesCount: Long?, unreadMessagesCount: Long?, hasNewActivitiesToShow: Long, + messageDraftText: String?, + messageDraftEditMessageId: String?, + messageDraftQuotedMessageId: String?, + messageDraftMentionList: List?, lastMessageId: String?, lastMessageContentType: MessageEntity.ContentType?, lastMessageDate: Instant?, @@ -96,24 +99,14 @@ data object ConversationDetailsWithEventsMapper { 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, @@ -147,11 +140,9 @@ data object ConversationDetailsWithEventsMapper { mlsGroupState = mlsGroupState, accessList = accessList, accessRoleList = accessRoleList, - unusedTeamId = unusedTeamId, mlsProposalTimer = mlsProposalTimer, mutedTime = mutedTime, creatorId = creatorId, - unusedLastModifiedDate = unusedLastModifiedDate, receiptMode = receiptMode, messageTimer = messageTimer, userMessageTimer = userMessageTimer, @@ -161,6 +152,8 @@ data object ConversationDetailsWithEventsMapper { mlsVerificationStatus = mlsVerificationStatus, proteusVerificationStatus = proteusVerificationStatus, legalHoldStatus = legalHoldStatus, + selfUserId = selfUserId, + interactionEnabled = interactionEnabled, ), unreadEvents = UnreadEventMapper.toConversationUnreadEntity( conversationId = qualifiedId, @@ -175,8 +168,7 @@ data object ConversationDetailsWithEventsMapper { if ( lastMessageId != null && lastMessageContentType != null && lastMessageDate != null && lastMessageVisibility != null && lastMessageSenderUserId != null && lastMessageIsEphemeral != null - && lastMessageIsSelfMessage != null && lastMessageIsMentioningSelfUser != null && lastMessageIsUnread != null - && lastMessageShouldNotify != null + && lastMessageIsSelfMessage != null && lastMessageIsMentioningSelfUser != null ) { MessageMapper.toPreviewEntity( id = lastMessageId, @@ -189,20 +181,20 @@ data object ConversationDetailsWithEventsMapper { senderName = lastMessageSenderName, senderConnectionStatus = lastMessageSenderConnectionStatus, senderIsDeleted = lastMessageSenderIsDeleted, - selfUserId = lastMessageSelfUserId, + selfUserId = selfUserId, isSelfMessage = lastMessageIsSelfMessage, memberChangeList = lastMessageMemberChangeList, memberChangeType = lastMessageMemberChangeType, updatedConversationName = lastMessageUpdateConversationName, - conversationName = lastMessageConversationName, + conversationName = name, isMentioningSelfUser = lastMessageIsMentioningSelfUser, isQuotingSelfUser = lastMessageIsQuotingSelfUser, text = lastMessageText, assetMimeType = lastMessageAssetMimeType, - isUnread = lastMessageIsUnread, - isNotified = lastMessageShouldNotify, - mutedStatus = lastMessageMutedStatus, - conversationType = lastMessageConversationType, + isUnread = lastMessageDate > lastReadDate, + isNotified = if (lastNotifiedMessageDate?.let { lastMessageDate > it } ?: false) 1 else 0, + mutedStatus = mutedStatus, + conversationType = type, ) } else null, messageDraft = if (!messageDraftText.isNullOrBlank()) { 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 81f62e07fa4..16330fe8667 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 @@ -20,7 +20,7 @@ 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.ConversationDetailsWithEventsQueries import com.wire.kalium.persistence.dao.conversation.ConversationExtensions.QueryConfig import com.wire.kalium.persistence.dao.message.KaliumPager import kotlin.coroutines.CoroutineContext @@ -42,7 +42,7 @@ interface ConversationExtensions { } internal class ConversationExtensionsImpl internal constructor( - private val queries: ConversationsQueries, + private val queries: ConversationDetailsWithEventsQueries, private val mapper: ConversationDetailsWithEventsMapper, private val coroutineContext: CoroutineContext, ) : ConversationExtensions { @@ -62,21 +62,47 @@ internal class ConversationExtensionsImpl internal constructor( 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( + countQuery = + if (searchQuery.isBlank()) { + queries.countConversationDetailsWithEvents( fromArchive = fromArchive, onlyInteractionsEnabled = onlyInteractionEnabled, - searchQuery = searchQuery, - newActivitiesOnTop = newActivitiesOnTop, conversationFilter = conversationFilter.name, - limit = limit, - offset = offset, - mapper = mapper::fromViewToModel, ) + } else { + queries.countConversationDetailsWithEventsFromSearch( + fromArchive = fromArchive, + onlyInteractionsEnabled = onlyInteractionEnabled, + conversationFilter = conversationFilter.name, + searchQuery = searchQuery + ) + }, + transacter = queries, + context = coroutineContext, + initialOffset = initialOffset, + queryProvider = { limit, offset -> + if (searchQuery.isBlank()) { + queries.selectConversationDetailsWithEvents( + fromArchive = fromArchive, + onlyInteractionsEnabled = onlyInteractionEnabled, + conversationFilter = conversationFilter.name, + newActivitiesOnTop = newActivitiesOnTop, + limit = limit, + offset = offset, + mapper = mapper::fromViewToModel, + ) + } else { + queries.selectConversationDetailsWithEventsFromSearch( + fromArchive = fromArchive, + onlyInteractionsEnabled = onlyInteractionEnabled, + conversationFilter = conversationFilter.name, + searchQuery = searchQuery, + newActivitiesOnTop = newActivitiesOnTop, + limit = limit, + offset = offset, + mapper = mapper::fromViewToModel, + ) + } } ) } 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 306abc1ed6e..8cea5874e4a 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 @@ -62,11 +62,9 @@ data object ConversationMapper { mlsGroupState: ConversationEntity.GroupState, accessList: List, accessRoleList: List, - unusedTeamId: String?, mlsProposalTimer: String?, mutedTime: Long, creatorId: String, - unusedLastModifiedDate: Instant, receiptMode: ConversationEntity.ReceiptMode, messageTimer: Long?, userMessageTimer: Long?, @@ -76,6 +74,8 @@ data object ConversationMapper { mlsVerificationStatus: ConversationEntity.VerificationStatus, proteusVerificationStatus: ConversationEntity.VerificationStatus, legalHoldStatus: ConversationEntity.LegalHoldStatus, + selfUserId: QualifiedIDEntity?, + interactionEnabled: Long, ): ConversationViewEntity = ConversationViewEntity( id = qualifiedId, name = name, diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/member/MemberDAO.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/member/MemberDAO.kt index fa1268acb40..7ea77f7010a 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/member/MemberDAO.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/member/MemberDAO.kt @@ -123,7 +123,7 @@ internal class MemberDAOImpl internal constructor( override suspend fun insertMembers(memberList: List, groupId: String) { withContext(coroutineContext) { conversationsQueries.selectByGroupId(groupId).executeAsOneOrNull()?.let { - nonSuspendInsertMembersWithQualifiedId(memberList, it.qualifiedId) + nonSuspendInsertMembersWithQualifiedId(memberList, it.qualified_id) } } } diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/UserDatabaseBuilder.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/UserDatabaseBuilder.kt index f00160be5e2..6dc85f9d0f5 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/UserDatabaseBuilder.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/UserDatabaseBuilder.kt @@ -193,6 +193,8 @@ class UserDatabaseBuilder internal constructor( conversationDetailsCache, conversationCache, database.conversationsQueries, + database.conversationDetailsQueries, + database.conversationDetailsWithEventsQueries, database.membersQueries, database.unreadEventsQueries, queriesContext, 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 95610f7f3f6..d01cd2e2e38 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 @@ -29,8 +29,8 @@ 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.ConversationDetailsWithEventsMapper import com.wire.kalium.persistence.dao.conversation.ConversationEntity import com.wire.kalium.persistence.dao.conversation.ConversationExtensions import com.wire.kalium.persistence.dao.conversation.ConversationExtensionsImpl @@ -65,7 +65,7 @@ class ConversationExtensionsTest : BaseDatabaseTest() { fun setUp() { deleteDatabase(selfUserId) val db = createDatabase(selfUserId, encryptedDBSecret, true) - val queries = db.database.conversationsQueries + val queries = db.database.conversationDetailsWithEventsQueries messageDAO = db.messageDAO messageDraftDAO = db.messageDraftDAO conversationDAO = db.conversationDAO 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 d67ff2ef108..b98958884d4 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 @@ -155,9 +155,9 @@ class ConversationDAOTest : BaseDatabaseTest() { fun givenExistingConversation_ThenConversationCanBeRetrievedByGroupID() = runTest { conversationDAO.insertConversation(conversationEntity2) insertTeamUserAndMember(team, user2, conversationEntity2.id) - val result = - conversationDAO.observeConversationByGroupID((conversationEntity2.protocolInfo as ConversationEntity.ProtocolInfo.MLS).groupId) - .first() + val result = conversationDAO.observeConversationDetailsByGroupID( + (conversationEntity2.protocolInfo as ConversationEntity.ProtocolInfo.MLS).groupId + ).first() assertEquals(conversationEntity2.toViewEntity(user2), result) } @@ -505,7 +505,7 @@ class ConversationDAOTest : BaseDatabaseTest() { // when conversationDAO.updateKeyingMaterial(conversationProtocolInfo.groupId, newUpdate) // then - assertEquals(expected, conversationDAO.observeConversationByGroupID(conversationProtocolInfo.groupId).first()?.protocolInfo) + assertEquals(expected, conversationDAO.observeConversationDetailsByGroupID(conversationProtocolInfo.groupId).first()?.protocolInfo) } @Test @@ -2240,7 +2240,7 @@ class ConversationDAOTest : BaseDatabaseTest() { conversationDAO.insertConversation(conversationEntity4) // when - val result = conversationDAO.getConversationByGroupID("call_subconversation_groupid") + val result = conversationDAO.getConversationDetailsByGroupID("call_subconversation_groupid") // then assertEquals(