From 4fe3fb7d512c10dd0cd0336ee39b2bf867397450 Mon Sep 17 00:00:00 2001 From: Jacob Persson <7156+typfel@users.noreply.github.com> Date: Wed, 11 Oct 2023 11:37:03 +0200 Subject: [PATCH 1/3] chore: update kalium ref --- kalium | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kalium b/kalium index d055503321f..a77539da744 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit d055503321f4021f0529f2d544522ec40f8f115a +Subproject commit a77539da744cc9231e1ac0c8054c97915c19509a From 2ae3a30e4d8bd79aea1d20f9af3c5a9fa163a6e1 Mon Sep 17 00:00:00 2001 From: Vitor Hugo Schwaab Date: Tue, 10 Oct 2023 18:17:24 +0200 Subject: [PATCH 2/3] feat(mls): redirect users to migrated one-on-one conversation This commit introduces a new ViewModel `ConversationMigrationViewModel.kt` for tracking conversation migrations. It observes details of a conversation and checks if this conversation was migrated to a different one-on-one conversation. If it was, it updates the target conversation with the ID of the active one-on-one conversation. --- .../banner/ConversationBannerViewModel.kt | 1 - .../ConversationMigrationViewModel.kt | 71 +++++++++++++++++++ 2 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/conversations/migration/ConversationMigrationViewModel.kt diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/banner/ConversationBannerViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/banner/ConversationBannerViewModel.kt index 051ec0bf562..d99a9c080ef 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/banner/ConversationBannerViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/banner/ConversationBannerViewModel.kt @@ -43,7 +43,6 @@ import kotlinx.coroutines.launch import javax.inject.Inject @OptIn(ExperimentalCoroutinesApi::class) -@Suppress("LongParameterList") @HiltViewModel class ConversationBannerViewModel @Inject constructor( override val savedStateHandle: SavedStateHandle, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/migration/ConversationMigrationViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/migration/ConversationMigrationViewModel.kt new file mode 100644 index 00000000000..42b2f029ffa --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/migration/ConversationMigrationViewModel.kt @@ -0,0 +1,71 @@ +/* + * Wire + * Copyright (C) 2023 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.ui.home.conversations.migration + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.viewModelScope +import com.wire.android.navigation.SavedStateViewModel +import com.wire.android.ui.home.conversations.ConversationNavArgs +import com.wire.android.ui.navArgs +import com.wire.kalium.logic.data.conversation.ConversationDetails +import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.data.id.QualifiedID +import com.wire.kalium.logic.feature.conversation.ObserveConversationDetailsUseCase +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch + +@HiltViewModel +class ConversationMigrationViewModel( + override val savedStateHandle: SavedStateHandle, + private val observeConversationDetails: ObserveConversationDetailsUseCase +) : SavedStateViewModel(savedStateHandle) { + + /** + * Represents the target conversation for a conversation migration. + * The target conversation is the active one-on-one conversation ID if the current conversation + * is migrated to a different conversation. + * If the conversation is not migrated, the target conversation is null. + */ + var targetConversation by mutableStateOf(null) + private set + + private val conversationNavArgs: ConversationNavArgs = savedStateHandle.navArgs() + private val conversationId: QualifiedID = conversationNavArgs.conversationId + + init { + viewModelScope.launch { + observeConversationDetails(conversationId) + .filterIsInstance() + .map { it.conversationDetails } + .filterIsInstance() + .collectLatest { + val activeOneOnOneConversationId = it.otherUser.activeOneOnOneConversationId + val wasThisConversationMigrated = activeOneOnOneConversationId != conversationId + if (wasThisConversationMigrated) { + targetConversation = activeOneOnOneConversationId + } + } + } + } +} From d8b5264d7dd95094ed6da5a9de66685be0ad528c Mon Sep 17 00:00:00 2001 From: Vitor Hugo Schwaab Date: Wed, 11 Oct 2023 17:39:48 +0200 Subject: [PATCH 3/3] feat(mls): navigate to migrated conversation --- .../com/wire/android/WireApplication.kt | 2 +- .../home/conversations/ConversationScreen.kt | 13 ++ .../ConversationMigrationViewModel.kt | 13 +- .../ConversationMigrationViewModelTest.kt | 113 ++++++++++++++++++ 4 files changed, 134 insertions(+), 7 deletions(-) create mode 100644 app/src/test/kotlin/com/wire/android/ui/home/conversations/migration/ConversationMigrationViewModelTest.kt diff --git a/app/src/main/kotlin/com/wire/android/WireApplication.kt b/app/src/main/kotlin/com/wire/android/WireApplication.kt index 5da7df6a04f..58a24257a3e 100644 --- a/app/src/main/kotlin/com/wire/android/WireApplication.kt +++ b/app/src/main/kotlin/com/wire/android/WireApplication.kt @@ -112,7 +112,7 @@ class WireApplication : Application(), Configuration.Provider { .detectDiskReads() .detectDiskWrites() .penaltyLog() - .penaltyDeath() +// .penaltyDeath() .build() ) StrictMode.setVmPolicy( diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt index 3bf769113a3..5d0c5805015 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt @@ -62,6 +62,7 @@ import com.wire.android.R import com.wire.android.appLogger import com.wire.android.media.audiomessage.AudioState import com.wire.android.model.SnackBarMessage +import com.wire.android.navigation.BackStackMode import com.wire.android.navigation.NavigationCommand import com.wire.android.navigation.Navigator import com.wire.android.ui.calling.common.MicrophoneBTPermissionsDeniedDialog @@ -75,6 +76,7 @@ import com.wire.android.ui.common.dialogs.calling.JoinAnywayDialog import com.wire.android.ui.common.dialogs.calling.OngoingActiveCallDialog import com.wire.android.ui.common.error.CoreFailureErrorDialog import com.wire.android.ui.common.snackbar.LocalSnackbarHostState +import com.wire.android.ui.destinations.ConversationScreenDestination import com.wire.android.ui.destinations.GroupConversationDetailsScreenDestination import com.wire.android.ui.destinations.InitiatingCallScreenDestination import com.wire.android.ui.destinations.MediaGalleryScreenDestination @@ -95,6 +97,7 @@ import com.wire.android.ui.home.conversations.info.ConversationInfoViewModel import com.wire.android.ui.home.conversations.info.ConversationInfoViewState import com.wire.android.ui.home.conversations.messages.ConversationMessagesViewModel import com.wire.android.ui.home.conversations.messages.ConversationMessagesViewState +import com.wire.android.ui.home.conversations.migration.ConversationMigrationViewModel import com.wire.android.ui.home.conversations.model.ExpirationStatus import com.wire.android.ui.home.conversations.model.UIMessage import com.wire.android.ui.home.conversations.selfdeletion.SelfDeletionMapper.toSelfDeletionDuration @@ -151,6 +154,7 @@ fun ConversationScreen( conversationCallViewModel: ConversationCallViewModel = hiltViewModel(), conversationMessagesViewModel: ConversationMessagesViewModel = hiltViewModel(), messageComposerViewModel: MessageComposerViewModel = hiltViewModel(), + conversationMigrationViewModel: ConversationMigrationViewModel = hiltViewModel(), groupDetailsScreenResultRecipient: ResultRecipient, mediaGalleryScreenResultRecipient: ResultRecipient, resultNavigator: ResultBackNavigator, @@ -177,6 +181,15 @@ fun ConversationScreen( } val context = LocalContext.current + conversationMigrationViewModel.migratedConversationId?.let { migratedConversationId -> + navigator.navigate( + NavigationCommand( + ConversationScreenDestination(migratedConversationId), + BackStackMode.REMOVE_CURRENT + ) + ) + } + with(conversationCallViewModel) { if (conversationCallViewState.shouldShowJoinAnywayDialog) { appLogger.i("showing showJoinAnywayDialog..") diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/migration/ConversationMigrationViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/migration/ConversationMigrationViewModel.kt index 42b2f029ffa..c7921075396 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/migration/ConversationMigrationViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/migration/ConversationMigrationViewModel.kt @@ -34,23 +34,24 @@ import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch +import javax.inject.Inject @HiltViewModel -class ConversationMigrationViewModel( +class ConversationMigrationViewModel @Inject constructor( override val savedStateHandle: SavedStateHandle, private val observeConversationDetails: ObserveConversationDetailsUseCase ) : SavedStateViewModel(savedStateHandle) { /** - * Represents the target conversation for a conversation migration. + * Represents the target conversation, after a conversation migration. * The target conversation is the active one-on-one conversation ID if the current conversation * is migrated to a different conversation. - * If the conversation is not migrated, the target conversation is null. + * If this conversation was not migrated to another one, the target conversation is null. */ - var targetConversation by mutableStateOf(null) + var migratedConversationId by mutableStateOf(null) private set - private val conversationNavArgs: ConversationNavArgs = savedStateHandle.navArgs() + private val conversationNavArgs = savedStateHandle.navArgs() private val conversationId: QualifiedID = conversationNavArgs.conversationId init { @@ -63,7 +64,7 @@ class ConversationMigrationViewModel( val activeOneOnOneConversationId = it.otherUser.activeOneOnOneConversationId val wasThisConversationMigrated = activeOneOnOneConversationId != conversationId if (wasThisConversationMigrated) { - targetConversation = activeOneOnOneConversationId + migratedConversationId = activeOneOnOneConversationId } } } diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/migration/ConversationMigrationViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/migration/ConversationMigrationViewModelTest.kt new file mode 100644 index 00000000000..629168d25d4 --- /dev/null +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/migration/ConversationMigrationViewModelTest.kt @@ -0,0 +1,113 @@ +/* + * Wire + * Copyright (C) 2023 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.ui.home.conversations.migration + +import androidx.lifecycle.SavedStateHandle +import com.wire.android.config.CoroutineTestExtension +import com.wire.android.config.NavigationTestExtension +import com.wire.android.framework.TestConversation +import com.wire.android.framework.TestUser +import com.wire.android.ui.home.conversations.ConversationNavArgs +import com.wire.android.ui.navArgs +import com.wire.kalium.logic.data.conversation.ConversationDetails +import com.wire.kalium.logic.data.conversation.LegalHoldStatus +import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.data.user.type.UserType +import com.wire.kalium.logic.feature.conversation.ObserveConversationDetailsUseCase +import io.mockk.MockKAnnotations +import io.mockk.coEvery +import io.mockk.every +import io.mockk.impl.annotations.MockK +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import org.amshove.kluent.shouldBe +import org.amshove.kluent.shouldBeEqualTo +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith + +@ExtendWith(CoroutineTestExtension::class) +@ExtendWith(NavigationTestExtension::class) +class ConversationMigrationViewModelTest { + + @Test + fun givenActiveOneOnOneMatchesCurrentConversation_thenMigratedConversationShouldBeNull() = runTest { + val (_, conversationMigrationViewModel) = arrange { + withConversationDetailsReturning( + ConversationDetails.OneOne( + conversation = TestConversation.ONE_ON_ONE, + otherUser = TestUser.OTHER_USER.copy(activeOneOnOneConversationId = conversationId), + legalHoldStatus = LegalHoldStatus.ENABLED, + userType = UserType.NONE, + unreadEventCount = mapOf(), + lastMessage = null + ) + ) + } + + conversationMigrationViewModel.migratedConversationId shouldBe null + } + + @Test + fun givenActiveOneOnOneDiffersFromCurrentConversation_thenMigratedConversationShouldBeTheOneInDetails() = runTest { + val expectedActiveOneOnOneId = ConversationId("expectedActiveOneOnOneId", "testDomain") + val (_, conversationMigrationViewModel) = arrange { + withConversationDetailsReturning( + ConversationDetails.OneOne( + conversation = TestConversation.ONE_ON_ONE, + otherUser = TestUser.OTHER_USER.copy(activeOneOnOneConversationId = expectedActiveOneOnOneId), + legalHoldStatus = LegalHoldStatus.ENABLED, + userType = UserType.NONE, + unreadEventCount = mapOf(), + lastMessage = null + ) + ) + } + + conversationMigrationViewModel.migratedConversationId shouldBeEqualTo expectedActiveOneOnOneId + } + + private class Arrangement(private val configure: Arrangement.() -> Unit) { + + @MockK + lateinit var observeConversationDetailsUseCase: ObserveConversationDetailsUseCase + + @MockK + lateinit var savedStateHandle: SavedStateHandle + + init { + MockKAnnotations.init(this) + every { savedStateHandle.navArgs() } returns ConversationNavArgs(conversationId) + } + + fun withConversationDetailsReturning(conversationDetails: ConversationDetails) = apply { + coEvery { observeConversationDetailsUseCase(conversationId) } returns + flowOf(ObserveConversationDetailsUseCase.Result.Success(conversationDetails)) + } + + fun arrange(): Pair = run { + configure() + this@Arrangement to ConversationMigrationViewModel(savedStateHandle, observeConversationDetailsUseCase) + } + } + + private companion object { + val conversationId = TestConversation.ID + + fun arrange(configure: Arrangement.() -> Unit) = Arrangement(configure).arrange() + } +}