diff --git a/app/src/main/kotlin/com/wire/android/WireApplication.kt b/app/src/main/kotlin/com/wire/android/WireApplication.kt index 9b2d2823d17..d67de8e4c97 100644 --- a/app/src/main/kotlin/com/wire/android/WireApplication.kt +++ b/app/src/main/kotlin/com/wire/android/WireApplication.kt @@ -29,6 +29,7 @@ import co.touchlab.kermit.platformLogWriter import com.wire.android.analytics.ObserveCurrentSessionAnalyticsUseCase import com.wire.android.datastore.GlobalDataStore import com.wire.android.datastore.UserDataStoreProvider +import com.wire.android.debug.DatabaseProfilingManager import com.wire.android.di.ApplicationScope import com.wire.android.di.KaliumCoreLogic import com.wire.android.feature.analytics.AnonymousAnalyticsManagerImpl @@ -89,6 +90,9 @@ class WireApplication : BaseApp() { @Inject lateinit var currentScreenManager: CurrentScreenManager + @Inject + lateinit var databaseProfilingManager: DatabaseProfilingManager + override val workManagerConfiguration: Configuration get() = Configuration.Builder() .setWorkerFactory(wireWorkerFactory.get()) @@ -183,6 +187,10 @@ class WireApplication : BaseApp() { logDeviceInformation() // 5. Verify if we can initialize Anonymous Analytics initializeAnonymousAnalytics() + // 6. Observe and update profiling when needed + globalAppScope.launch { + databaseProfilingManager.observeAndUpdateProfiling() + } } private fun initializeAnonymousAnalytics() { diff --git a/app/src/main/kotlin/com/wire/android/debug/DatabaseProfilingManager.kt b/app/src/main/kotlin/com/wire/android/debug/DatabaseProfilingManager.kt new file mode 100644 index 00000000000..e6fcc1aa28c --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/debug/DatabaseProfilingManager.kt @@ -0,0 +1,56 @@ +/* + * 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.android.debug + +import com.wire.android.datastore.GlobalDataStore +import com.wire.android.di.KaliumCoreLogic +import com.wire.kalium.logic.CoreLogic +import com.wire.kalium.logic.data.user.UserId +import com.wire.kalium.logic.functional.mapToRightOr +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.scan +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class DatabaseProfilingManager @Inject constructor( + @KaliumCoreLogic private val coreLogic: CoreLogic, + private val globalDataStore: GlobalDataStore, +) { + + suspend fun observeAndUpdateProfiling() { + globalDataStore.isLoggingEnabled() + .flatMapLatest { isLoggingEnabled -> + coreLogic.getGlobalScope().sessionRepository.allValidSessionsFlow() + .mapToRightOr(emptyList()) + .map { it.map { it.userId } } + .scan(emptyList()) { previousList, currentList -> currentList - previousList.toSet() } + .map { userIds -> isLoggingEnabled to userIds } + } + .filter { (_, userIds) -> userIds.isNotEmpty() } + .distinctUntilChanged() + .collect { (isLoggingEnabled, userIds) -> + userIds.forEach { userId -> + coreLogic.getSessionScope(userId).debug.changeProfiling(isLoggingEnabled) + } + } + } +} diff --git a/app/src/main/kotlin/com/wire/android/mapper/ConversationMapper.kt b/app/src/main/kotlin/com/wire/android/mapper/ConversationMapper.kt index 4098405e5c8..abda5b931af 100644 --- a/app/src/main/kotlin/com/wire/android/mapper/ConversationMapper.kt +++ b/app/src/main/kotlin/com/wire/android/mapper/ConversationMapper.kt @@ -96,6 +96,7 @@ fun ConversationDetailsWithEvents.toConversationItem( ), userId = conversationDetails.otherUser.id, blockingState = conversationDetails.otherUser.BlockState, + isUserDeleted = conversationDetails.otherUser.deleted, teamId = conversationDetails.otherUser.teamId, isArchived = conversationDetails.conversation.archived, mlsVerificationStatus = conversationDetails.conversation.mlsVerificationStatus, diff --git a/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/conversation/ConversationSheetContent.kt b/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/conversation/ConversationSheetContent.kt index 4d43febe51e..36ca87da01e 100644 --- a/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/conversation/ConversationSheetContent.kt +++ b/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/conversation/ConversationSheetContent.kt @@ -103,7 +103,8 @@ sealed class ConversationTypeDetail { data class Private( val avatarAsset: UserAvatarAsset?, val userId: UserId, - val blockingState: BlockingState + val blockingState: BlockingState, + val isUserDeleted: Boolean ) : ConversationTypeDetail() data class Connection(val avatarAsset: UserAvatarAsset?) : ConversationTypeDetail() @@ -131,7 +132,8 @@ data class ConversationSheetContent( fun canEditNotifications(): Boolean = isSelfUserMember && ((conversationTypeDetail is ConversationTypeDetail.Private - && (conversationTypeDetail.blockingState != BlockingState.BLOCKED)) + && (conversationTypeDetail.blockingState != BlockingState.BLOCKED) + && !conversationTypeDetail.isUserDeleted) || conversationTypeDetail is ConversationTypeDetail.Group) fun canDeleteGroup(): Boolean { @@ -142,8 +144,11 @@ data class ConversationSheetContent( fun canLeaveTheGroup(): Boolean = conversationTypeDetail is ConversationTypeDetail.Group && isSelfUserMember - fun canBlockUser(): Boolean = - conversationTypeDetail is ConversationTypeDetail.Private && conversationTypeDetail.blockingState == BlockingState.NOT_BLOCKED + fun canBlockUser(): Boolean { + return conversationTypeDetail is ConversationTypeDetail.Private + && conversationTypeDetail.blockingState == BlockingState.NOT_BLOCKED + && !conversationTypeDetail.isUserDeleted + } fun canUnblockUser(): Boolean = conversationTypeDetail is ConversationTypeDetail.Private && conversationTypeDetail.blockingState == BlockingState.BLOCKED diff --git a/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/conversation/ConversationSheetState.kt b/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/conversation/ConversationSheetState.kt index f5bd110bc79..250e5c7439b 100644 --- a/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/conversation/ConversationSheetState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/conversation/ConversationSheetState.kt @@ -93,9 +93,10 @@ fun rememberConversationSheetState( } else conversationInfo.name, mutingConversationState = mutedStatus, conversationTypeDetail = ConversationTypeDetail.Private( - userAvatarData.asset, - userId, - blockingState + avatarAsset = userAvatarData.asset, + userId = userId, + blockingState = blockingState, + isUserDeleted = isUserDeleted ), isTeamConversation = isTeamConversation, selfRole = Conversation.Member.Role.Member, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModel.kt index 466664122da..0814e5f74e6 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModel.kt @@ -24,6 +24,7 @@ import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.paging.PagingData +import androidx.paging.cachedIn import androidx.paging.insertSeparators import androidx.paging.map import com.wire.android.BuildConfig @@ -211,10 +212,11 @@ class ConversationListViewModelImpl @AssistedInject constructor( } } .flowOn(dispatcher.io()) + .cachedIn(viewModelScope) private var notPaginatedConversationListState by mutableStateOf(ConversationListState.NotPaginated()) - override val conversationListState: ConversationListState - get() = if (usePagination) { + override val conversationListState: ConversationListState = + if (usePagination) { ConversationListState.Paginated( conversations = conversationsPaginatedFlow, domain = currentAccount.domain diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/common/ConversationItemFactory.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/common/ConversationItemFactory.kt index 45db07413b1..d462bc90234 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/common/ConversationItemFactory.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/common/ConversationItemFactory.kt @@ -516,7 +516,8 @@ fun PreviewPrivateConversationItemWithBlockedBadge() = WireTheme { isArchived = false, mlsVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED, proteusVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED, - isFavorite = false + isFavorite = false, + isUserDeleted = false ), modifier = Modifier, isSelectableItem = false, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/common/ConversationList.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/common/ConversationList.kt index 730ef25eaf7..ed04d4d72bc 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/common/ConversationList.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/common/ConversationList.kt @@ -224,7 +224,8 @@ fun previewConversationList(count: Int, startIndex: Int = 0, unread: Boolean = f mlsVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED, proteusVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED, searchQuery = searchQuery, - isFavorite = false + isFavorite = false, + isUserDeleted = false ) ) } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/model/ConversationItem.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/model/ConversationItem.kt index d5ae38f7625..66bced8c774 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/model/ConversationItem.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/model/ConversationItem.kt @@ -75,6 +75,7 @@ sealed class ConversationItem : ConversationFolderItem { val conversationInfo: ConversationInfo, val userId: UserId, val blockingState: BlockingState, + val isUserDeleted: Boolean, override val conversationId: ConversationId, override val mutedStatus: MutedConversationStatus, override val showLegalHoldIndicator: Boolean = false, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/state/MessageComposerStateHolder.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/state/MessageComposerStateHolder.kt index 0ea6534dc7d..02b0152216f 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/state/MessageComposerStateHolder.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/state/MessageComposerStateHolder.kt @@ -55,15 +55,6 @@ fun rememberMessageComposerStateHolder( val messageTextFieldValue = remember { mutableStateOf(TextFieldValue()) } - LaunchedEffect(draftMessageComposition.draftText) { - if (draftMessageComposition.draftText.isNotBlank()) { - messageTextFieldValue.value = messageTextFieldValue.value.copy( - text = draftMessageComposition.draftText, - selection = TextRange(draftMessageComposition.draftText.length) // Place cursor at the end of the new text - ) - } - } - val messageCompositionHolder = remember { mutableStateOf( MessageCompositionHolder( @@ -77,6 +68,23 @@ fun rememberMessageComposerStateHolder( ) ) } + + LaunchedEffect(draftMessageComposition.draftText) { + if (draftMessageComposition.draftText.isNotBlank()) { + messageTextFieldValue.value = messageTextFieldValue.value.copy( + text = draftMessageComposition.draftText, + selection = TextRange(draftMessageComposition.draftText.length) // Place cursor at the end of the new text + ) + } + + if (draftMessageComposition.selectedMentions.isNotEmpty()) { + messageCompositionHolder.value.setMentions( + draftMessageComposition.draftText, + draftMessageComposition.selectedMentions.map { it.intoMessageMention() } + ) + } + } + LaunchedEffect(Unit) { messageCompositionHolder.value.handleMessageTextUpdates() } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/state/MessageCompositionHolder.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/state/MessageCompositionHolder.kt index 9c0b29b89fb..9b5590244c3 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/state/MessageCompositionHolder.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/state/MessageCompositionHolder.kt @@ -241,13 +241,20 @@ class MessageCompositionHolder( ) messageComposition.update { it.copy( - selectedMentions = mentions.mapNotNull { it.toUiMention(editMessageText) }, + selectedMentions = mentions.mapNotNull { mention -> mention.toUiMention(editMessageText) }, editMessageId = messageId ) } onSaveDraft(messageComposition.value.toDraft(editMessageText)) } + fun setMentions(editMessageText: String, mentions: List) { + messageComposition.update { + it.copy(selectedMentions = mentions.mapNotNull { mention -> mention.toUiMention(editMessageText) }) + } + onSaveDraft(messageComposition.value.toDraft(editMessageText)) + } + fun addOrRemoveMessageMarkdown( markdown: RichTextMarkdown, ) { diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreen.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreen.kt index 98d5a77978d..6a869b01690 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreen.kt @@ -598,7 +598,8 @@ fun ContentFooter( exit = fadeOut(), ) { // TODO show open conversation button for service bots after AR-2135 - if (!state.isMetadataEmpty() && state.membership != Membership.Service && !state.isTemporaryUser()) { + val isNotTemporaryAndNotDeleted = !state.isTemporaryUser() && !state.isDeletedUser + if (!state.isMetadataEmpty() && state.membership != Membership.Service && isNotTemporaryAndNotDeleted) { Surface( shadowElevation = dimensions().bottomNavigationShadowElevation, color = MaterialTheme.wireColorScheme.background diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreenViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreenViewModel.kt index 9f809c80afc..69effb5e0db 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreenViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreenViewModel.kt @@ -398,15 +398,17 @@ class OtherUserProfileScreenViewModel @Inject constructor( isUnderLegalHold = otherUser.isUnderLegalHold, expiresAt = otherUser.expiresAt, accentId = otherUser.accentId, + isDeletedUser = otherUser.deleted, conversationSheetContent = conversation?.let { ConversationSheetContent( title = otherUser.name.orEmpty(), conversationId = conversation.id, mutingConversationState = conversation.mutedStatus, conversationTypeDetail = ConversationTypeDetail.Private( - userAvatarAsset, - userId, - otherUser.BlockState + avatarAsset = userAvatarAsset, + userId = userId, + blockingState = otherUser.BlockState, + isUserDeleted = otherUser.deleted ), isTeamConversation = conversation.isTeamGroup(), selfRole = Conversation.Member.Role.Member, diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileState.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileState.kt index 491108acc7f..672093c0b55 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileState.kt @@ -55,7 +55,8 @@ data class OtherUserProfileState( val isConversationStarted: Boolean = false, val expiresAt: Instant? = null, val accentId: Int = -1, - val errorLoadingUser: ErrorLoadingUser? = null + val errorLoadingUser: ErrorLoadingUser? = null, + val isDeletedUser: Boolean = false ) { fun updateMuteStatus(status: MutedConversationStatus): OtherUserProfileState { return conversationSheetContent?.let { diff --git a/app/src/test/kotlin/com/wire/android/debug/DatabaseProfilingManagerTest.kt b/app/src/test/kotlin/com/wire/android/debug/DatabaseProfilingManagerTest.kt new file mode 100644 index 00000000000..b0dc851cc0d --- /dev/null +++ b/app/src/test/kotlin/com/wire/android/debug/DatabaseProfilingManagerTest.kt @@ -0,0 +1,187 @@ +/* + * 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.android.debug + +import com.wire.android.config.CoroutineTestExtension +import com.wire.android.datastore.GlobalDataStore +import com.wire.kalium.logic.CoreLogic +import com.wire.kalium.logic.StorageFailure +import com.wire.kalium.logic.data.auth.AccountInfo +import com.wire.kalium.logic.data.user.UserId +import com.wire.kalium.logic.functional.Either +import io.mockk.MockKAnnotations +import io.mockk.coEvery +import io.mockk.impl.annotations.MockK +import io.mockk.mockk +import kotlinx.collections.immutable.PersistentMap +import kotlinx.collections.immutable.persistentMapOf +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest +import org.amshove.kluent.internal.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith + +@ExtendWith(CoroutineTestExtension::class) +class DatabaseProfilingManagerTest { + + @Test + fun `given valid session and logging enabled, when observing, then profiling should be enabled`() = + runTest { + // given + val account = AccountInfo.Valid(UserId("user", "domain")) + val (arrangement, databaseProfilingManager) = Arrangement() + .withAllValidSessions(flowOf(Either.Right(listOf(account)))) + .withIsLoggingEnabled(flowOf(true)) + .arrange() + + // when + val job = launch { + databaseProfilingManager.observeAndUpdateProfiling() + } + advanceUntilIdle() + // then + assertEquals(true, arrangement.profilingValues[account.userId]) + job.cancel() + } + + @Test + fun `given valid session and logging disabled, when observing, then profiling is disabled`() = + runTest { + // given + val account = AccountInfo.Valid(UserId("user", "domain")) + val (arrangement, databaseProfilingManager) = Arrangement() + .withAllValidSessions(flowOf(Either.Right(listOf(account)))) + .withIsLoggingEnabled(flowOf(false)) + .arrange() + // when + val job = launch { + databaseProfilingManager.observeAndUpdateProfiling() + } + advanceUntilIdle() + // then + assertEquals(false, arrangement.profilingValues[account.userId]) + job.cancel() + } + + @Test + fun `given valid session, when observing and logging changes from disabled to enabled, then profiling is enabled`() = + runTest { + // given + val account = AccountInfo.Valid(UserId("user", "domain")) + val (arrangement, databaseProfilingManager) = Arrangement() + .withAllValidSessions(flowOf(Either.Right(listOf(account)))) + .withIsLoggingEnabled(flowOf(false)) + .arrange() + // when + val job = launch { + databaseProfilingManager.observeAndUpdateProfiling() + } + arrangement.withIsLoggingEnabled(flowOf(true)) + advanceUntilIdle() + // then + assertEquals(true, arrangement.profilingValues[account.userId]) + job.cancel() + } + + @Test + fun `given two valid sessions, when observing and logging changes from disabled to enabled, then profiling is enabled for both`() = + runTest { + // given + val account1 = AccountInfo.Valid(UserId("user1", "domain")) + val account2 = AccountInfo.Valid(UserId("user2", "domain")) + val (arrangement, databaseProfilingManager) = Arrangement() + .withAllValidSessions(flowOf(Either.Right(listOf(account1, account2)))) + .withIsLoggingEnabled(flowOf(false)) + .arrange() + // when + val job = launch { + databaseProfilingManager.observeAndUpdateProfiling() + } + arrangement.withIsLoggingEnabled(flowOf(true)) + advanceUntilIdle() + // then + assertEquals(true, arrangement.profilingValues[account1.userId]) + assertEquals(true, arrangement.profilingValues[account2.userId]) + job.cancel() + } + + @Test + fun `given valid session and logging enabled, when observing and new session appears, then profiling is enabled for both`() = + runTest { + // given + val account1 = AccountInfo.Valid(UserId("user1", "domain")) + val account2 = AccountInfo.Valid(UserId("user2", "domain")) + val validSessionsFlow = MutableStateFlow(Either.Right(listOf(account1))) + val (arrangement, databaseProfilingManager) = Arrangement() + .withAllValidSessions(validSessionsFlow) + .withIsLoggingEnabled(flowOf(true)) + .arrange() + // when + val job = launch { + databaseProfilingManager.observeAndUpdateProfiling() + } + validSessionsFlow.value = Either.Right(listOf(account1, account2)) + advanceUntilIdle() + // then + assertEquals(true, arrangement.profilingValues[account1.userId]) + assertEquals(true, arrangement.profilingValues[account2.userId]) + job.cancel() + } + + private class Arrangement { + + @MockK + lateinit var coreLogic: CoreLogic + + @MockK + private lateinit var globalDataStore: GlobalDataStore + + var profilingValues: PersistentMap = persistentMapOf() + private set + + init { + MockKAnnotations.init(this, relaxed = true, relaxUnitFun = true) + coEvery { coreLogic.getSessionScope(any()).debug.changeProfiling(any()) } answers { + profilingValues = profilingValues.put(firstArg(), secondArg()) + } + coEvery { coreLogic.getSessionScope(any()) } answers { + val userId = firstArg() + mockk { + coEvery { debug.changeProfiling(any()) } answers { + val profilingValue = firstArg() + profilingValues = profilingValues.put(userId, profilingValue) + } + } + } + } + + fun withIsLoggingEnabled(isLoggingEnabledFlow: Flow) = apply { + coEvery { globalDataStore.isLoggingEnabled() } returns isLoggingEnabledFlow + } + + fun withAllValidSessions(allValidSessionsFlow: Flow>>) = apply { + coEvery { coreLogic.getGlobalScope().sessionRepository.allValidSessionsFlow() } returns allValidSessionsFlow + } + + fun arrange() = this to DatabaseProfilingManager(coreLogic, globalDataStore) + } +} diff --git a/app/src/test/kotlin/com/wire/android/framework/TestConversationItem.kt b/app/src/test/kotlin/com/wire/android/framework/TestConversationItem.kt index 0d95340a56a..848fb683703 100644 --- a/app/src/test/kotlin/com/wire/android/framework/TestConversationItem.kt +++ b/app/src/test/kotlin/com/wire/android/framework/TestConversationItem.kt @@ -45,7 +45,8 @@ object TestConversationItem { isArchived = false, mlsVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED, proteusVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED, - isFavorite = false + isFavorite = false, + isUserDeleted = false ) val GROUP = ConversationItem.GroupConversation( diff --git a/app/src/test/kotlin/com/wire/android/ui/CallActivityViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/CallActivityViewModelTest.kt index 1c7345672a2..448d1f987fb 100644 --- a/app/src/test/kotlin/com/wire/android/ui/CallActivityViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/CallActivityViewModelTest.kt @@ -117,6 +117,7 @@ class CallActivityViewModelTest { .arrange() viewModel.switchAccountIfNeeded(userId, arrangement.switchAccountActions) + advanceUntilIdle() coVerify(inverse = true) { arrangement.accountSwitch(any()) } } @@ -132,6 +133,7 @@ class CallActivityViewModelTest { .arrange() viewModel.switchAccountIfNeeded(UserId("anotherUser", "domain"), arrangement.switchAccountActions) + advanceUntilIdle() coVerify(exactly = if (switchedToAnotherAccountCalled) 1 else 0) { arrangement.switchAccountActions.switchedToAnotherAccount() diff --git a/app/src/test/kotlin/com/wire/android/ui/common/bottomsheet/conversation/ConversationSheetContentTest.kt b/app/src/test/kotlin/com/wire/android/ui/common/bottomsheet/conversation/ConversationSheetContentTest.kt index a95c20a4985..35cd7180406 100644 --- a/app/src/test/kotlin/com/wire/android/ui/common/bottomsheet/conversation/ConversationSheetContentTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/common/bottomsheet/conversation/ConversationSheetContentTest.kt @@ -17,33 +17,22 @@ */ package com.wire.android.ui.common.bottomsheet.conversation +import com.wire.android.framework.TestUser import com.wire.android.ui.home.conversations.details.GroupConversationDetailsViewModelTest.Companion.testGroup +import com.wire.android.ui.home.conversationslist.model.BlockingState import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.id.TeamId import kotlinx.coroutines.test.runTest import org.amshove.kluent.internal.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test class ConversationSheetContentTest { @Test fun givenTitleIsEmptyAndTheGroupSizeIsOne_whenCallingIsTheGroupAbandoned_returnsTrue() = runTest { - val details = testGroup.copy(conversation = testGroup.conversation.copy(teamId = TeamId("team_id"))) - - val givenConversationSheetContent = ConversationSheetContent( - title = "", - conversationId = details.conversation.id, - mutingConversationState = details.conversation.mutedStatus, - conversationTypeDetail = ConversationTypeDetail.Group(details.conversation.id, false), - selfRole = Conversation.Member.Role.Member, - isTeamConversation = details.conversation.isTeamGroup(), - isArchived = false, - protocol = Conversation.ProtocolInfo.Proteus, - mlsVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED, - proteusVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED, - isUnderLegalHold = false, - isFavorite = false - ) + val givenConversationSheetContent = createGroupSheetContent("") val givenParticipantsCount = 1 assertEquals(true, givenConversationSheetContent.isAbandonedOneOnOneConversation(givenParticipantsCount)) @@ -51,13 +40,137 @@ class ConversationSheetContentTest { @Test fun givenTitleIsEmptyAndTheGroupSizeIsGtOne_whenCallingIsTheGroupAbandoned_returnsFalse() = runTest { - val details = testGroup.copy(conversation = testGroup.conversation.copy(teamId = TeamId("team_id"))) + val givenConversationSheetContent = createGroupSheetContent("") + val givenParticipantsCount = 3 + + assertEquals(false, givenConversationSheetContent.isAbandonedOneOnOneConversation(givenParticipantsCount)) + } + + @Test + fun givenTitleIsNotEmptyAndTheGroupSizeIsOne_whenCallingIsTheGroupAbandoned_returnsFalse() = runTest { + val givenConversationSheetContent = createGroupSheetContent("notEmpty") + val givenParticipantsCount = 3 + + assertEquals(false, givenConversationSheetContent.isAbandonedOneOnOneConversation(givenParticipantsCount)) + } + + @Test + fun givenPrivateConversationWithoutBlockedAndNotDeletedUser_whenCanDeleteUserIsInvoked_thenReturnsTrue() = runTest { + // given + val conversationSheetContent = + createPrivateSheetContent(blockingState = BlockingState.NOT_BLOCKED, isUserDeleted = false) + + // when + val canBlockUser = conversationSheetContent.canBlockUser() + + // then + assertTrue(canBlockUser) + } + + @Test + fun givenGroupConversation_whenCanDeleteUserIsInvoked_thenReturnsFalse() = runTest { + // given + val conversationSheetContent = createGroupSheetContent("") + + // when + val canBlockUser = conversationSheetContent.canBlockUser() + + // then + assertFalse(canBlockUser) + } + + @Test + fun givenPrivateConversationWithBlockedAndNotDeletedUser_whenCanDeleteUserIsInvoked_thenReturnsFalse() = runTest { + // given + val conversationSheetContent = + createPrivateSheetContent(blockingState = BlockingState.BLOCKED, isUserDeleted = false) + + // when + val canBlockUser = conversationSheetContent.canBlockUser() - val givenConversationSheetContent = ConversationSheetContent( - title = "", + // then + assertFalse(canBlockUser) + } + + @Test + fun givenPrivateConversationWithoutBlockedAndDeletedUser_whenCanDeleteUserIsInvoked_thenReturnsFalse() = runTest { + // given + val conversationSheetContent = + createPrivateSheetContent(blockingState = BlockingState.NOT_BLOCKED, isUserDeleted = true) + + // when + val canBlockUser = conversationSheetContent.canBlockUser() + + // then + assertFalse(canBlockUser) + } + + @Test + fun givenPrivateConversationWithoutBlockedAndNotDeletedUser_whenCanEditNotificationsIsInvoked_thenReturnsTrue() = runTest { + // given + val conversationSheetContent = + createPrivateSheetContent(blockingState = BlockingState.NOT_BLOCKED, isUserDeleted = false) + + // when + val canEditNotifications = conversationSheetContent.canEditNotifications() + + // then + assertTrue(canEditNotifications) + } + + @Test + fun givenGroupConversation_whenCanEditNotificationsIsInvoked_thenReturnsTrue() = runTest { + // given + val conversationSheetContent = createGroupSheetContent("") + + // when + val canEditNotifications = conversationSheetContent.canEditNotifications() + + // then + assertTrue(canEditNotifications) + } + + @Test + fun givenPrivateConversationWithBlockedAndNotDeletedUser_whenCanEditNotificationsIsInvoked_thenReturnsFalse() = runTest { + // given + val conversationSheetContent = + createPrivateSheetContent(blockingState = BlockingState.BLOCKED, isUserDeleted = false) + + // when + val canEditNotifications = conversationSheetContent.canEditNotifications() + + // then + assertFalse(canEditNotifications) + } + + @Test + fun givenPrivateConversationWithoutBlockedAndDeletedUser_whenCanEditNotificationsIsInvoked_thenReturnsFalse() = runTest { + // given + val conversationSheetContent = + createPrivateSheetContent(blockingState = BlockingState.BLOCKED, isUserDeleted = false) + + // when + val canEditNotifications = conversationSheetContent.canEditNotifications() + + // then + assertFalse(canEditNotifications) + } + + private fun createPrivateSheetContent( + blockingState: BlockingState, + isUserDeleted: Boolean + ): ConversationSheetContent { + val details = testGroup.copy(conversation = testGroup.conversation.copy(teamId = TeamId("team_id"))) + return ConversationSheetContent( + title = "notEmpty", conversationId = details.conversation.id, mutingConversationState = details.conversation.mutedStatus, - conversationTypeDetail = ConversationTypeDetail.Group(details.conversation.id, false), + conversationTypeDetail = ConversationTypeDetail.Private( + avatarAsset = null, + userId = TestUser.USER_ID, + blockingState = blockingState, + isUserDeleted = isUserDeleted + ), selfRole = Conversation.Member.Role.Member, isTeamConversation = details.conversation.isTeamGroup(), isArchived = false, @@ -67,17 +180,15 @@ class ConversationSheetContentTest { isUnderLegalHold = false, isFavorite = false ) - val givenParticipantsCount = 3 - - assertEquals(false, givenConversationSheetContent.isAbandonedOneOnOneConversation(givenParticipantsCount)) } - @Test - fun givenTitleIsNotEmptyAndTheGroupSizeIsOne_whenCallingIsTheGroupAbandoned_returnsFalse() = runTest { + private fun createGroupSheetContent( + title: String + ): ConversationSheetContent { val details = testGroup.copy(conversation = testGroup.conversation.copy(teamId = TeamId("team_id"))) - val givenConversationSheetContent = ConversationSheetContent( - title = "notEmpty", + return ConversationSheetContent( + title = title, conversationId = details.conversation.id, mutingConversationState = details.conversation.mutedStatus, conversationTypeDetail = ConversationTypeDetail.Group(details.conversation.id, false), @@ -90,8 +201,5 @@ class ConversationSheetContentTest { isUnderLegalHold = false, isFavorite = false ) - val givenParticipantsCount = 3 - - assertEquals(false, givenConversationSheetContent.isAbandonedOneOnOneConversation(givenParticipantsCount)) } } diff --git a/kalium b/kalium index 26b7d4b4dcf..0667f9b780a 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 26b7d4b4dcf2b50b64a3979e6211094a7a5d63d4 +Subproject commit 0667f9b780a8262768b0c37af3d49d4f83c55701