From 539cc46fa3279792074648e84846260cd918594c Mon Sep 17 00:00:00 2001 From: ohassine Date: Wed, 20 Nov 2024 11:53:58 +0100 Subject: [PATCH 01/18] feat: hide personal to team migration feature if not supported by backend --- .../com/wire/android/di/accountScoped/UserModule.kt | 6 ++++++ .../main/kotlin/com/wire/android/ui/home/HomeViewModel.kt | 8 ++++++++ .../android/ui/userprofile/self/SelfUserProfileScreen.kt | 2 +- .../android/ui/userprofile/self/SelfUserProfileState.kt | 1 + .../ui/userprofile/self/SelfUserProfileViewModel.kt | 8 ++++++++ 5 files changed, 24 insertions(+), 1 deletion(-) diff --git a/app/src/main/kotlin/com/wire/android/di/accountScoped/UserModule.kt b/app/src/main/kotlin/com/wire/android/di/accountScoped/UserModule.kt index f1d6baba694..58a61010375 100644 --- a/app/src/main/kotlin/com/wire/android/di/accountScoped/UserModule.kt +++ b/app/src/main/kotlin/com/wire/android/di/accountScoped/UserModule.kt @@ -33,6 +33,7 @@ import com.wire.kalium.logic.feature.e2ei.usecase.GetUserE2eiCertificatesUseCase import com.wire.kalium.logic.feature.e2ei.usecase.IsOtherUserE2EIVerifiedUseCase import com.wire.kalium.logic.feature.e2ei.usecase.ObserveCertificateRevocationForSelfClientUseCase import com.wire.kalium.logic.feature.featureConfig.FeatureFlagsSyncWorker +import com.wire.kalium.logic.feature.personaltoteamaccount.IsPersonalToTeamAccountSupportedByBackendUseCase import com.wire.kalium.logic.feature.publicuser.GetAllContactsUseCase import com.wire.kalium.logic.feature.publicuser.GetKnownUserUseCase import com.wire.kalium.logic.feature.publicuser.RefreshUsersWithoutMetadataUseCase @@ -240,6 +241,11 @@ class UserModule { fun provideFeatureFlagsSyncWorker(userScope: UserScope): FeatureFlagsSyncWorker = userScope.featureFlagsSyncWorker + @ViewModelScoped + @Provides + fun provideIsPersonalToTeamAccountSupportedByBackendUseCase(userScope: UserScope): IsPersonalToTeamAccountSupportedByBackendUseCase = + userScope.isPersonalToTeamAccountSupportedByBackend + @ViewModelScoped @Provides fun provideObserveCertificateRevocationForSelfClientUseCase(userScope: UserScope): ObserveCertificateRevocationForSelfClientUseCase = diff --git a/app/src/main/kotlin/com/wire/android/ui/home/HomeViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/HomeViewModel.kt index 7d0444dea44..19461b08be9 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/HomeViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/HomeViewModel.kt @@ -37,6 +37,7 @@ import com.wire.android.util.ui.WireSessionImageLoader import com.wire.kalium.logic.feature.client.NeedsToRegisterClientUseCase import com.wire.kalium.logic.feature.legalhold.LegalHoldStateForSelfUser import com.wire.kalium.logic.feature.legalhold.ObserveLegalHoldStateForSelfUserUseCase +import com.wire.kalium.logic.feature.personaltoteamaccount.IsPersonalToTeamAccountSupportedByBackendUseCase import com.wire.kalium.logic.feature.user.GetSelfUserUseCase import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.collectLatest @@ -52,6 +53,7 @@ class HomeViewModel @Inject constructor( private val dataStore: UserDataStore, private val getSelf: GetSelfUserUseCase, private val needsToRegisterClient: NeedsToRegisterClientUseCase, + private val isPersonalToTeamAccountSupportedByBackend: IsPersonalToTeamAccountSupportedByBackendUseCase, private val observeLegalHoldStatusForSelfUser: ObserveLegalHoldStateForSelfUserUseCase, private val wireSessionImageLoader: WireSessionImageLoader, private val shouldTriggerMigrationForUser: ShouldTriggerMigrationForUserUserCase, @@ -80,6 +82,12 @@ class HomeViewModel @Inject constructor( private fun observeCreateTeamIndicator() { viewModelScope.launch { + if (!isPersonalToTeamAccountSupportedByBackend()) { + homeState = homeState.copy( + shouldShowCreateTeamUnreadIndicator = false + ) + return@launch + } getSelf().first().let { selfUser -> val isPersonalUser = selfUser.teamId == null if (isPersonalUser) { diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileScreen.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileScreen.kt index dd913cfac34..70a44582e10 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileScreen.kt @@ -234,7 +234,7 @@ private fun SelfUserProfileContent( .fillMaxHeight() .scrollable(state = scrollState, orientation = Orientation.Vertical) ) { - if (state.teamName == null) { + if (state.isAbleToMigrateToTeamAccount) { stickyHeader { Column( modifier = Modifier diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileState.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileState.kt index 1e763c7c6a1..c5ffb5c647a 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileState.kt @@ -39,6 +39,7 @@ data class SelfUserProfileState( val isAvatarLoading: Boolean = false, val maxAccountsReached: Boolean = false, // todo. cleanup unused code val isReadOnlyAccount: Boolean = true, + val isAbleToMigrateToTeamAccount: Boolean = false, val isLoggingOut: Boolean = false, val legalHoldStatus: LegalHoldUIState = LegalHoldUIState.None, val accentId: Int = -1 diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModel.kt index 85f41060d40..77138c43f2a 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModel.kt @@ -54,6 +54,7 @@ import com.wire.kalium.logic.feature.call.usecase.EndCallUseCase import com.wire.kalium.logic.feature.call.usecase.ObserveEstablishedCallsUseCase import com.wire.kalium.logic.feature.legalhold.LegalHoldStateForSelfUser import com.wire.kalium.logic.feature.legalhold.ObserveLegalHoldStateForSelfUserUseCase +import com.wire.kalium.logic.feature.personaltoteamaccount.IsPersonalToTeamAccountSupportedByBackendUseCase import com.wire.kalium.logic.feature.team.GetUpdatedSelfTeamUseCase import com.wire.kalium.logic.feature.user.GetSelfUserUseCase import com.wire.kalium.logic.feature.user.IsReadOnlyAccountUseCase @@ -84,6 +85,7 @@ class SelfUserProfileViewModel @Inject constructor( private val dataStore: UserDataStore, private val getSelf: GetSelfUserUseCase, private val getSelfTeam: GetUpdatedSelfTeamUseCase, + private val isPersonalToTeamAccountSupportedByBackend: IsPersonalToTeamAccountSupportedByBackendUseCase, private val observeValidAccounts: ObserveValidAccountsUseCase, private val updateStatus: UpdateSelfAvailabilityStatusUseCase, private val logout: LogoutUseCase, @@ -109,6 +111,7 @@ class SelfUserProfileViewModel @Inject constructor( init { viewModelScope.launch { fetchSelfUser() + checkIfUserAbleToMigrateToTeamAccount() observeEstablishedCall() fetchIsReadOnlyAccount() observeLegalHoldStatus() @@ -116,6 +119,11 @@ class SelfUserProfileViewModel @Inject constructor( } } + private suspend fun checkIfUserAbleToMigrateToTeamAccount() { + val isAbleToMigrateToTeamAccount = isPersonalToTeamAccountSupportedByBackend() && userProfileState.teamName.isNullOrBlank() + userProfileState = userProfileState.copy(isAbleToMigrateToTeamAccount = isAbleToMigrateToTeamAccount) + } + private suspend fun fetchIsReadOnlyAccount() { val isReadOnlyAccount = isReadOnlyAccount() userProfileState = userProfileState.copy(isReadOnlyAccount = isReadOnlyAccount) From d468507c8127feb702415079e846d609abdaac41 Mon Sep 17 00:00:00 2001 From: ohassine Date: Wed, 20 Nov 2024 11:55:26 +0100 Subject: [PATCH 02/18] feat: update kalium reference --- kalium | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kalium b/kalium index e617c90fb7c..fc811436874 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit e617c90fb7cd79e554946aa865e46a9ed9a78b67 +Subproject commit fc8114368741b4c8a4667c8011564818fb92d73b From 1bc844377a3a2f89f801507ac83cd0adc770abbb Mon Sep 17 00:00:00 2001 From: ohassine Date: Wed, 20 Nov 2024 12:30:07 +0100 Subject: [PATCH 03/18] feat: unit test --- .../com/wire/android/ui/home/HomeViewModelTest.kt | 12 +++++++++++- .../self/SelfUserProfileViewModelArrangement.kt | 7 ++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/app/src/test/kotlin/com/wire/android/ui/home/HomeViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/HomeViewModelTest.kt index ba7d91972ce..ea4d97363e9 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/HomeViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/HomeViewModelTest.kt @@ -31,6 +31,7 @@ import com.wire.kalium.logic.data.user.UserAvailabilityStatus import com.wire.kalium.logic.feature.client.NeedsToRegisterClientUseCase import com.wire.kalium.logic.feature.legalhold.LegalHoldStateForSelfUser import com.wire.kalium.logic.feature.legalhold.ObserveLegalHoldStateForSelfUserUseCase +import com.wire.kalium.logic.feature.personaltoteamaccount.IsPersonalToTeamAccountSupportedByBackendUseCase import com.wire.kalium.logic.feature.user.GetSelfUserUseCase import io.mockk.MockKAnnotations import io.mockk.coEvery @@ -143,6 +144,9 @@ class HomeViewModelTest { @MockK lateinit var analyticsManager: AnonymousAnalyticsManager + @MockK + lateinit var isPersonalToTeamAccountSupportedByBackend: IsPersonalToTeamAccountSupportedByBackendUseCase + private val viewModel by lazy { HomeViewModel( savedStateHandle = savedStateHandle, @@ -153,19 +157,25 @@ class HomeViewModelTest { observeLegalHoldStatusForSelfUser = observeLegalHoldStatusForSelfUser, wireSessionImageLoader = wireSessionImageLoader, shouldTriggerMigrationForUser = shouldTriggerMigrationForUser, - analyticsManager = analyticsManager + analyticsManager = analyticsManager, + isPersonalToTeamAccountSupportedByBackend = isPersonalToTeamAccountSupportedByBackend ) } init { MockKAnnotations.init(this, relaxUnitFun = true) withGetSelf(flowOf(TestUser.SELF_USER)) + withIsPersonalToTeamAccountSupportedByBackendReturning(true) } fun withGetSelf(result: Flow) = apply { coEvery { getSelf.invoke() } returns result } + fun withIsPersonalToTeamAccountSupportedByBackendReturning(result: Boolean) = apply { + coEvery { isPersonalToTeamAccountSupportedByBackend.invoke() } returns result + } + fun withLegalHoldStatus(result: Flow) = apply { coEvery { observeLegalHoldStatusForSelfUser.invoke() } returns result } diff --git a/app/src/test/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModelArrangement.kt b/app/src/test/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModelArrangement.kt index 5c58262e8de..2bc3470e45c 100644 --- a/app/src/test/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModelArrangement.kt +++ b/app/src/test/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModelArrangement.kt @@ -35,6 +35,7 @@ import com.wire.kalium.logic.feature.call.usecase.EndCallUseCase import com.wire.kalium.logic.feature.call.usecase.ObserveEstablishedCallsUseCase import com.wire.kalium.logic.feature.legalhold.LegalHoldStateForSelfUser import com.wire.kalium.logic.feature.legalhold.ObserveLegalHoldStateForSelfUserUseCase +import com.wire.kalium.logic.feature.personaltoteamaccount.IsPersonalToTeamAccountSupportedByBackendUseCase import com.wire.kalium.logic.feature.team.GetUpdatedSelfTeamUseCase import com.wire.kalium.logic.feature.user.GetSelfUserUseCase import com.wire.kalium.logic.feature.user.IsReadOnlyAccountUseCase @@ -101,6 +102,9 @@ class SelfUserProfileViewModelArrangement { @MockK lateinit var anonymousAnalyticsManager: AnonymousAnalyticsManager + @MockK + lateinit var isPersonalToTeamAccountSupportedByBackend: IsPersonalToTeamAccountSupportedByBackendUseCase + private val viewModel by lazy { SelfUserProfileViewModel( selfUserId = TestUser.SELF_USER.id, @@ -121,7 +125,8 @@ class SelfUserProfileViewModelArrangement { notificationManager = notificationManager, globalDataStore = globalDataStore, qualifiedIdMapper = qualifiedIdMapper, - anonymousAnalyticsManager = anonymousAnalyticsManager + anonymousAnalyticsManager = anonymousAnalyticsManager, + isPersonalToTeamAccountSupportedByBackend = isPersonalToTeamAccountSupportedByBackend ) } From 66162d15585830c3ef99ebe96a73520196337d65 Mon Sep 17 00:00:00 2001 From: ohassine Date: Thu, 21 Nov 2024 10:42:12 +0100 Subject: [PATCH 04/18] chore: address comments --- .../android/di/accountScoped/UserModule.kt | 4 ++-- .../com/wire/android/ui/home/HomeViewModel.kt | 20 ++++++++----------- .../self/SelfUserProfileViewModel.kt | 4 ++-- .../wire/android/ui/home/HomeViewModelTest.kt | 12 +++++------ .../SelfUserProfileViewModelArrangement.kt | 4 ++-- 5 files changed, 20 insertions(+), 24 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/di/accountScoped/UserModule.kt b/app/src/main/kotlin/com/wire/android/di/accountScoped/UserModule.kt index 58a61010375..e4ed7d01bfa 100644 --- a/app/src/main/kotlin/com/wire/android/di/accountScoped/UserModule.kt +++ b/app/src/main/kotlin/com/wire/android/di/accountScoped/UserModule.kt @@ -33,7 +33,7 @@ import com.wire.kalium.logic.feature.e2ei.usecase.GetUserE2eiCertificatesUseCase import com.wire.kalium.logic.feature.e2ei.usecase.IsOtherUserE2EIVerifiedUseCase import com.wire.kalium.logic.feature.e2ei.usecase.ObserveCertificateRevocationForSelfClientUseCase import com.wire.kalium.logic.feature.featureConfig.FeatureFlagsSyncWorker -import com.wire.kalium.logic.feature.personaltoteamaccount.IsPersonalToTeamAccountSupportedByBackendUseCase +import com.wire.kalium.logic.feature.personaltoteamaccount.CanMigrateFromPersonalToTeamUseCase import com.wire.kalium.logic.feature.publicuser.GetAllContactsUseCase import com.wire.kalium.logic.feature.publicuser.GetKnownUserUseCase import com.wire.kalium.logic.feature.publicuser.RefreshUsersWithoutMetadataUseCase @@ -243,7 +243,7 @@ class UserModule { @ViewModelScoped @Provides - fun provideIsPersonalToTeamAccountSupportedByBackendUseCase(userScope: UserScope): IsPersonalToTeamAccountSupportedByBackendUseCase = + fun provideIsPersonalToTeamAccountSupportedByBackendUseCase(userScope: UserScope): CanMigrateFromPersonalToTeamUseCase = userScope.isPersonalToTeamAccountSupportedByBackend @ViewModelScoped diff --git a/app/src/main/kotlin/com/wire/android/ui/home/HomeViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/HomeViewModel.kt index 19461b08be9..4e4667ad186 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/HomeViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/HomeViewModel.kt @@ -37,7 +37,7 @@ import com.wire.android.util.ui.WireSessionImageLoader import com.wire.kalium.logic.feature.client.NeedsToRegisterClientUseCase import com.wire.kalium.logic.feature.legalhold.LegalHoldStateForSelfUser import com.wire.kalium.logic.feature.legalhold.ObserveLegalHoldStateForSelfUserUseCase -import com.wire.kalium.logic.feature.personaltoteamaccount.IsPersonalToTeamAccountSupportedByBackendUseCase +import com.wire.kalium.logic.feature.personaltoteamaccount.CanMigrateFromPersonalToTeamUseCase import com.wire.kalium.logic.feature.user.GetSelfUserUseCase import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.collectLatest @@ -53,7 +53,7 @@ class HomeViewModel @Inject constructor( private val dataStore: UserDataStore, private val getSelf: GetSelfUserUseCase, private val needsToRegisterClient: NeedsToRegisterClientUseCase, - private val isPersonalToTeamAccountSupportedByBackend: IsPersonalToTeamAccountSupportedByBackendUseCase, + private val canMigrateFromPersonalToTeam: CanMigrateFromPersonalToTeamUseCase, private val observeLegalHoldStatusForSelfUser: ObserveLegalHoldStateForSelfUserUseCase, private val wireSessionImageLoader: WireSessionImageLoader, private val shouldTriggerMigrationForUser: ShouldTriggerMigrationForUserUserCase, @@ -82,21 +82,17 @@ class HomeViewModel @Inject constructor( private fun observeCreateTeamIndicator() { viewModelScope.launch { - if (!isPersonalToTeamAccountSupportedByBackend()) { + if (!canMigrateFromPersonalToTeam()) { homeState = homeState.copy( shouldShowCreateTeamUnreadIndicator = false ) return@launch } - getSelf().first().let { selfUser -> - val isPersonalUser = selfUser.teamId == null - if (isPersonalUser) { - dataStore.isCreateTeamNoticeRead().collect { isRead -> - homeState = homeState.copy( - shouldShowCreateTeamUnreadIndicator = !isRead - ) - } - } + + dataStore.isCreateTeamNoticeRead().collect { isRead -> + homeState = homeState.copy( + shouldShowCreateTeamUnreadIndicator = !isRead + ) } } } diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModel.kt index 77138c43f2a..1bfa32beaef 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModel.kt @@ -54,7 +54,7 @@ import com.wire.kalium.logic.feature.call.usecase.EndCallUseCase import com.wire.kalium.logic.feature.call.usecase.ObserveEstablishedCallsUseCase import com.wire.kalium.logic.feature.legalhold.LegalHoldStateForSelfUser import com.wire.kalium.logic.feature.legalhold.ObserveLegalHoldStateForSelfUserUseCase -import com.wire.kalium.logic.feature.personaltoteamaccount.IsPersonalToTeamAccountSupportedByBackendUseCase +import com.wire.kalium.logic.feature.personaltoteamaccount.CanMigrateFromPersonalToTeamUseCase import com.wire.kalium.logic.feature.team.GetUpdatedSelfTeamUseCase import com.wire.kalium.logic.feature.user.GetSelfUserUseCase import com.wire.kalium.logic.feature.user.IsReadOnlyAccountUseCase @@ -85,7 +85,7 @@ class SelfUserProfileViewModel @Inject constructor( private val dataStore: UserDataStore, private val getSelf: GetSelfUserUseCase, private val getSelfTeam: GetUpdatedSelfTeamUseCase, - private val isPersonalToTeamAccountSupportedByBackend: IsPersonalToTeamAccountSupportedByBackendUseCase, + private val isPersonalToTeamAccountSupportedByBackend: CanMigrateFromPersonalToTeamUseCase, private val observeValidAccounts: ObserveValidAccountsUseCase, private val updateStatus: UpdateSelfAvailabilityStatusUseCase, private val logout: LogoutUseCase, diff --git a/app/src/test/kotlin/com/wire/android/ui/home/HomeViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/HomeViewModelTest.kt index ea4d97363e9..26592e0d177 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/HomeViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/HomeViewModelTest.kt @@ -31,7 +31,7 @@ import com.wire.kalium.logic.data.user.UserAvailabilityStatus import com.wire.kalium.logic.feature.client.NeedsToRegisterClientUseCase import com.wire.kalium.logic.feature.legalhold.LegalHoldStateForSelfUser import com.wire.kalium.logic.feature.legalhold.ObserveLegalHoldStateForSelfUserUseCase -import com.wire.kalium.logic.feature.personaltoteamaccount.IsPersonalToTeamAccountSupportedByBackendUseCase +import com.wire.kalium.logic.feature.personaltoteamaccount.CanMigrateFromPersonalToTeamUseCase import com.wire.kalium.logic.feature.user.GetSelfUserUseCase import io.mockk.MockKAnnotations import io.mockk.coEvery @@ -145,7 +145,7 @@ class HomeViewModelTest { lateinit var analyticsManager: AnonymousAnalyticsManager @MockK - lateinit var isPersonalToTeamAccountSupportedByBackend: IsPersonalToTeamAccountSupportedByBackendUseCase + lateinit var canMigrateFromPersonalToTeam: CanMigrateFromPersonalToTeamUseCase private val viewModel by lazy { HomeViewModel( @@ -158,22 +158,22 @@ class HomeViewModelTest { wireSessionImageLoader = wireSessionImageLoader, shouldTriggerMigrationForUser = shouldTriggerMigrationForUser, analyticsManager = analyticsManager, - isPersonalToTeamAccountSupportedByBackend = isPersonalToTeamAccountSupportedByBackend + isPersonalToTeamAccountSupportedByBackend = canMigrateFromPersonalToTeam ) } init { MockKAnnotations.init(this, relaxUnitFun = true) withGetSelf(flowOf(TestUser.SELF_USER)) - withIsPersonalToTeamAccountSupportedByBackendReturning(true) + withCanMigrateFromPersonalToTeamReturning(true) } fun withGetSelf(result: Flow) = apply { coEvery { getSelf.invoke() } returns result } - fun withIsPersonalToTeamAccountSupportedByBackendReturning(result: Boolean) = apply { - coEvery { isPersonalToTeamAccountSupportedByBackend.invoke() } returns result + private fun withCanMigrateFromPersonalToTeamReturning(result: Boolean) = apply { + coEvery { canMigrateFromPersonalToTeam.invoke() } returns result } fun withLegalHoldStatus(result: Flow) = apply { diff --git a/app/src/test/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModelArrangement.kt b/app/src/test/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModelArrangement.kt index 2bc3470e45c..91a99ddf11c 100644 --- a/app/src/test/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModelArrangement.kt +++ b/app/src/test/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModelArrangement.kt @@ -35,7 +35,7 @@ import com.wire.kalium.logic.feature.call.usecase.EndCallUseCase import com.wire.kalium.logic.feature.call.usecase.ObserveEstablishedCallsUseCase import com.wire.kalium.logic.feature.legalhold.LegalHoldStateForSelfUser import com.wire.kalium.logic.feature.legalhold.ObserveLegalHoldStateForSelfUserUseCase -import com.wire.kalium.logic.feature.personaltoteamaccount.IsPersonalToTeamAccountSupportedByBackendUseCase +import com.wire.kalium.logic.feature.personaltoteamaccount.CanMigrateFromPersonalToTeamUseCase import com.wire.kalium.logic.feature.team.GetUpdatedSelfTeamUseCase import com.wire.kalium.logic.feature.user.GetSelfUserUseCase import com.wire.kalium.logic.feature.user.IsReadOnlyAccountUseCase @@ -103,7 +103,7 @@ class SelfUserProfileViewModelArrangement { lateinit var anonymousAnalyticsManager: AnonymousAnalyticsManager @MockK - lateinit var isPersonalToTeamAccountSupportedByBackend: IsPersonalToTeamAccountSupportedByBackendUseCase + lateinit var isPersonalToTeamAccountSupportedByBackend: CanMigrateFromPersonalToTeamUseCase private val viewModel by lazy { SelfUserProfileViewModel( From af4983337f96492d1b5f0f0359d1bbb6dffde9be Mon Sep 17 00:00:00 2001 From: ohassine Date: Thu, 21 Nov 2024 11:50:26 +0100 Subject: [PATCH 05/18] chore: kalium reference --- kalium | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kalium b/kalium index fc811436874..6aac45d4e47 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit fc8114368741b4c8a4667c8011564818fb92d73b +Subproject commit 6aac45d4e47874aafb004c06c5ccb14dd5fa8d8f From abe3a39f4aa208b0166415f56a91c3743326844f Mon Sep 17 00:00:00 2001 From: ohassine Date: Fri, 22 Nov 2024 09:15:11 +0100 Subject: [PATCH 06/18] chore: kalium reference --- kalium | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kalium b/kalium index 6aac45d4e47..08c0cffe74b 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 6aac45d4e47874aafb004c06c5ccb14dd5fa8d8f +Subproject commit 08c0cffe74b9523c7464e3645eb79f2ca7d59d3f From ef4ce7e3e6260dae98e21a385a548fc1c3f851ad Mon Sep 17 00:00:00 2001 From: ohassine Date: Fri, 22 Nov 2024 10:14:36 +0100 Subject: [PATCH 07/18] chore: kalium reference --- kalium | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kalium b/kalium index 08c0cffe74b..e271934a097 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 08c0cffe74b9523c7464e3645eb79f2ca7d59d3f +Subproject commit e271934a097c0e64d6085e11090286318ab10767 From 2c4163cdee3f95f645502c9e2f0df5e8567e446f Mon Sep 17 00:00:00 2001 From: ohassine Date: Tue, 26 Nov 2024 10:13:53 +0100 Subject: [PATCH 08/18] chore: update kalium reference --- kalium | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kalium b/kalium index e271934a097..c7b96a2584d 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit e271934a097c0e64d6085e11090286318ab10767 +Subproject commit c7b96a2584d6cba1619501b917a90b757785b7ac From 1e774367e57759438ea08925a10b3c548c69fe03 Mon Sep 17 00:00:00 2001 From: ohassine Date: Tue, 26 Nov 2024 10:20:08 +0100 Subject: [PATCH 09/18] chore: Empty-Commit From 4ac44fd979dc810ea82d8aff6bb13122e89cdc12 Mon Sep 17 00:00:00 2001 From: ohassine Date: Tue, 26 Nov 2024 10:44:49 +0100 Subject: [PATCH 10/18] chore: unit test --- .../test/kotlin/com/wire/android/ui/home/HomeViewModelTest.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/test/kotlin/com/wire/android/ui/home/HomeViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/HomeViewModelTest.kt index 26592e0d177..28198da601e 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/HomeViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/HomeViewModelTest.kt @@ -158,7 +158,7 @@ class HomeViewModelTest { wireSessionImageLoader = wireSessionImageLoader, shouldTriggerMigrationForUser = shouldTriggerMigrationForUser, analyticsManager = analyticsManager, - isPersonalToTeamAccountSupportedByBackend = canMigrateFromPersonalToTeam + canMigrateFromPersonalToTeam = canMigrateFromPersonalToTeam ) } @@ -174,6 +174,7 @@ class HomeViewModelTest { private fun withCanMigrateFromPersonalToTeamReturning(result: Boolean) = apply { coEvery { canMigrateFromPersonalToTeam.invoke() } returns result + coEvery { dataStore.isCreateTeamNoticeRead() } returns flowOf(false) } fun withLegalHoldStatus(result: Flow) = apply { From 0adfb1889b1b6f52fbf4bb3b81589a6a3b668f8a Mon Sep 17 00:00:00 2001 From: ohassine Date: Tue, 26 Nov 2024 14:55:28 +0100 Subject: [PATCH 11/18] chore: unit test --- .../ui/userprofile/teammigration/TeamMigrationViewModel.kt | 2 -- .../teammigration/TeamMigrationViewModelTest.kt | 7 +++---- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/TeamMigrationViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/TeamMigrationViewModel.kt index b8652aa171b..9b8fefd2dc9 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/TeamMigrationViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/TeamMigrationViewModel.kt @@ -17,7 +17,6 @@ */ package com.wire.android.ui.userprofile.teammigration -import androidx.compose.foundation.text.input.setTextAndSelectAll import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue @@ -100,7 +99,6 @@ class TeamMigrationViewModel @Inject constructor( ).let { result -> when (result) { is MigrateFromPersonalToTeamResult.Success -> { - teamMigrationState.teamNameTextState.setTextAndSelectAll(result.teamName) onSuccess() } diff --git a/app/src/test/kotlin/com/wire/android/ui/userprofile/teammigration/TeamMigrationViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/userprofile/teammigration/TeamMigrationViewModelTest.kt index 4def3b04039..05d1f2e396d 100644 --- a/app/src/test/kotlin/com/wire/android/ui/userprofile/teammigration/TeamMigrationViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/userprofile/teammigration/TeamMigrationViewModelTest.kt @@ -219,14 +219,13 @@ class TeamMigrationViewModelTest { fun arrange() = this to TeamMigrationViewModel( anonymousAnalyticsManager = anonymousAnalyticsManager, migrateFromPersonalToTeam = migrateFromPersonalToTeam, - ).also { viewModel -> + ) + .also { viewModel -> viewModel.teamMigrationState.teamNameTextState.setTextAndPlaceCursorAtEnd(TEAM_NAME) } fun withMigrateFromPersonalToTeamSuccess() = apply { - coEvery { migrateFromPersonalToTeam(any()) } returns MigrateFromPersonalToTeamResult.Success( - TEAM_NAME - ) + coEvery { migrateFromPersonalToTeam(any()) } returns MigrateFromPersonalToTeamResult.Success } fun withMigrateFromPersonalToTeamError() = apply { From 9298370a65cb233d0f5213b122a2160ca0038fd3 Mon Sep 17 00:00:00 2001 From: ohassine Date: Tue, 26 Nov 2024 15:26:38 +0100 Subject: [PATCH 12/18] chore: detekt --- .../ui/userprofile/teammigration/TeamMigrationViewModelTest.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/test/kotlin/com/wire/android/ui/userprofile/teammigration/TeamMigrationViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/userprofile/teammigration/TeamMigrationViewModelTest.kt index 05d1f2e396d..efec0349927 100644 --- a/app/src/test/kotlin/com/wire/android/ui/userprofile/teammigration/TeamMigrationViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/userprofile/teammigration/TeamMigrationViewModelTest.kt @@ -219,8 +219,7 @@ class TeamMigrationViewModelTest { fun arrange() = this to TeamMigrationViewModel( anonymousAnalyticsManager = anonymousAnalyticsManager, migrateFromPersonalToTeam = migrateFromPersonalToTeam, - ) - .also { viewModel -> + ).also { viewModel -> viewModel.teamMigrationState.teamNameTextState.setTextAndPlaceCursorAtEnd(TEAM_NAME) } From 2337d1d46270425ca6ecc3ff90b5127c6d1a5e27 Mon Sep 17 00:00:00 2001 From: ohassine Date: Tue, 26 Nov 2024 16:06:34 +0100 Subject: [PATCH 13/18] chore: update kalium reference --- kalium | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kalium b/kalium index c7b96a2584d..786a7b83e84 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit c7b96a2584d6cba1619501b917a90b757785b7ac +Subproject commit 786a7b83e84663909c6c40b3f6c72263fe935661 From a1ce79bd2a60cca90637361f5d57cce7cf48b15b Mon Sep 17 00:00:00 2001 From: ohassine Date: Fri, 29 Nov 2024 20:46:29 +0100 Subject: [PATCH 14/18] feat: mention while typing --- .../textfield/MentionDeletionHandler.kt | 50 ----- .../ui/common/textfield/WireTextField.kt | 12 +- .../textfield/mention/MentionAdjuster.kt | 95 +++++++++ .../mention/MentionSelectionManager.kt | 49 +++++ .../mention/MentionUpdateCoordinator.kt | 87 +++++++++ .../messagecomposer/EnabledMessageComposer.kt | 16 +- .../messagecomposer/MessageComposerInput.kt | 2 +- .../state/MessageCompositionHolder.kt | 22 ++- .../textfield/MentionDeletionHandlerTest.kt | 97 ---------- .../textfield/mention/MentionAdjusterTest.kt | 163 ++++++++++++++++ .../mention/MentionSelectionManagerTest.kt | 105 ++++++++++ .../mention/MentionUpdateCoordinatorTest.kt | 182 ++++++++++++++++++ 12 files changed, 708 insertions(+), 172 deletions(-) delete mode 100644 app/src/main/kotlin/com/wire/android/ui/common/textfield/MentionDeletionHandler.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/common/textfield/mention/MentionAdjuster.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/common/textfield/mention/MentionSelectionManager.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/common/textfield/mention/MentionUpdateCoordinator.kt delete mode 100644 app/src/test/kotlin/com/wire/android/ui/common/textfield/MentionDeletionHandlerTest.kt create mode 100644 app/src/test/kotlin/com/wire/android/ui/common/textfield/mention/MentionAdjusterTest.kt create mode 100644 app/src/test/kotlin/com/wire/android/ui/common/textfield/mention/MentionSelectionManagerTest.kt create mode 100644 app/src/test/kotlin/com/wire/android/ui/common/textfield/mention/MentionUpdateCoordinatorTest.kt diff --git a/app/src/main/kotlin/com/wire/android/ui/common/textfield/MentionDeletionHandler.kt b/app/src/main/kotlin/com/wire/android/ui/common/textfield/MentionDeletionHandler.kt deleted file mode 100644 index 9f34070fc6f..00000000000 --- a/app/src/main/kotlin/com/wire/android/ui/common/textfield/MentionDeletionHandler.kt +++ /dev/null @@ -1,50 +0,0 @@ -/* - * 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.ui.common.textfield - -import androidx.compose.ui.text.TextRange - -object MentionDeletionHandler { - @Suppress("ReturnCount") - fun handle( - oldText: String, - newText: String, - oldSelection: TextRange, - mentions: List - ): String { - if (oldText == newText) { - // No change in text, only cursor movement, return as is - return oldText - } - for (mention in mentions) { - // Find the start position of the mention in the text - val mentionStart = oldText.indexOf(mention) - - if (mentionStart == -1) continue - - val mentionEnd = mentionStart + mention.length - - // Check if the selection (i.e., user's cursor position) is inside the mention's range - if (oldSelection.start in mentionStart + 1..mentionEnd || oldSelection.end in mentionStart + 1..mentionEnd) { - // If the user is deleting inside the mention, remove the entire mention - return oldText.removeRange(mentionStart, mentionEnd) - } - } - return newText - } -} diff --git a/app/src/main/kotlin/com/wire/android/ui/common/textfield/WireTextField.kt b/app/src/main/kotlin/com/wire/android/ui/common/textfield/WireTextField.kt index 4ee35be59bb..2350ba611a3 100644 --- a/app/src/main/kotlin/com/wire/android/ui/common/textfield/WireTextField.kt +++ b/app/src/main/kotlin/com/wire/android/ui/common/textfield/WireTextField.kt @@ -222,17 +222,7 @@ internal fun WireTextField( innerBasicTextField = { _, textFieldModifier, decoratorBox -> BasicTextField( value = textFieldValue.value, - onValueChange = { newText -> - val mentionsByName = mentions.map { it.handler } - val updatedText = - MentionDeletionHandler.handle( - textFieldValue.value.text, - newText.text, - textFieldValue.value.selection, - mentionsByName - ) - onValueChange(TextFieldValue(updatedText, newText.selection)) - }, + onValueChange = onValueChange, textStyle = textStyle.copy( color = colors.textColor(state = state).value, textDirection = TextDirection.ContentOrLtr diff --git a/app/src/main/kotlin/com/wire/android/ui/common/textfield/mention/MentionAdjuster.kt b/app/src/main/kotlin/com/wire/android/ui/common/textfield/mention/MentionAdjuster.kt new file mode 100644 index 00000000000..db29d619562 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/common/textfield/mention/MentionAdjuster.kt @@ -0,0 +1,95 @@ +/* + * 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.ui.common.textfield.mention + +import androidx.compose.ui.text.TextRange +import com.wire.android.ui.home.conversations.model.UIMention + +/** + * Adjusts mentions based on changes in the text. + */ +class MentionAdjuster { + + /** + * Adjusts mentions based on the deleted text. + * @param mentions The list of mentions in the text. + * @param deletedLength The length of the deleted text. + * @param text The new text after deletion. + * @param selection The current selection. + */ + fun adjustMentionsForDeletion( + mentions: List, + deletedLength: Int, + text: String, + selection: TextRange + ): Pair, TextRange> { + val updatedMentions = mutableListOf() + var newSelection = selection + + mentions.forEach { mention -> + if (selection.start >= mention.start + mention.length) { + // No change for mentions that are before the deleted text. + updatedMentions.add(mention) + // if the cursor is at the end of the mention, select te mention + if (mention.start + mention.length == selection.end) { + newSelection = TextRange(mention.start, mention.start + mention.length) + } + } else { + // Handle mentions that were affected by the deletion and adjusting their start position. + val newStart = mention.start - deletedLength + if (newStart >= 0) { + val mentionSubstring = text.substring(newStart, newStart + mention.length) + if (mentionSubstring == mention.handler) { + updatedMentions.add(mention.copy(start = newStart)) + } + } + } + } + + return Pair(updatedMentions, newSelection) + } + + /** + * Adjusts mentions based on the inserted text. + * @param mentions The list of mentions in the text. + * @param text The new text after insertion. + * @param addedLength The length of the inserted text. + * @param selection The current selection. + */ + fun adjustMentionsForInsertion( + mentions: List, + text: String, + selection: TextRange, + addedLength: Int + ): Pair, TextRange> { + val updatedMentions = mutableListOf() + // Adjust mentions based on the inserted text. + mentions.forEach { mention -> + val mentionSubstring = text.substring(mention.start, mention.start + mention.length) + if (mentionSubstring == mention.handler) { + // No change if the mention text remains the same. + updatedMentions.add(mention) + } else { + // Handle mentions that were affected by the insertion and shift their start position. + updatedMentions.add(mention.copy(start = mention.start + addedLength)) + } + } + + return Pair(updatedMentions, selection) + } +} diff --git a/app/src/main/kotlin/com/wire/android/ui/common/textfield/mention/MentionSelectionManager.kt b/app/src/main/kotlin/com/wire/android/ui/common/textfield/mention/MentionSelectionManager.kt new file mode 100644 index 00000000000..d6c2f299ca3 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/common/textfield/mention/MentionSelectionManager.kt @@ -0,0 +1,49 @@ +/* + * 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.ui.common.textfield.mention + +import androidx.compose.ui.text.TextRange +import com.wire.android.ui.home.conversations.model.UIMention + +/** + * Manages the selection for mentions. + */ +class MentionSelectionManager { + fun updateSelectionForMention( + oldSelection: TextRange, + newSelection: TextRange, + mentions: List + ): TextRange { + if (oldSelection != newSelection) { + mentions.forEach { mention -> + if (newSelection.isInside(mention)) { + return TextRange(mention.start, mention.start + mention.length) + } + } + } + return newSelection + } + + /** + * Extension function to check if the selection is inside the mention's range. + */ + private fun TextRange.isInside(mention: UIMention): Boolean { + return this.start in mention.start until mention.start + mention.length && + this.end in mention.start until mention.start + mention.length + } +} diff --git a/app/src/main/kotlin/com/wire/android/ui/common/textfield/mention/MentionUpdateCoordinator.kt b/app/src/main/kotlin/com/wire/android/ui/common/textfield/mention/MentionUpdateCoordinator.kt new file mode 100644 index 00000000000..b5f57c4bec6 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/common/textfield/mention/MentionUpdateCoordinator.kt @@ -0,0 +1,87 @@ +/* + * 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.ui.common.textfield.mention + +import androidx.compose.ui.text.input.TextFieldValue +import com.wire.android.ui.home.conversations.model.UIMention + +/** + * Manages the updates to a `TextFieldValue` based on changes in the text and mentions. + */ +class MentionUpdateCoordinator( + private val mentionAdjuster: MentionAdjuster = MentionAdjuster(), + private val selectionManager: MentionSelectionManager = MentionSelectionManager() +) { + fun handle( + oldTextFieldValue: TextFieldValue, + newTextFieldValue: TextFieldValue, + mentions: List, + updateMentions: (List) -> Unit + ): TextFieldValue { + if (newTextFieldValue.text.isEmpty()) { + updateMentions(emptyList()) + return newTextFieldValue + } + + // If there are no mentions, simply return the new TextFieldValue. + if (mentions.isEmpty()) { + return newTextFieldValue + } + + val deletedLength = oldTextFieldValue.text.length - newTextFieldValue.text.length + val addedLength = newTextFieldValue.text.length - oldTextFieldValue.text.length + + when { + deletedLength > 0 -> { + val result = mentionAdjuster.adjustMentionsForDeletion( + mentions = mentions, + deletedLength = deletedLength, + text = newTextFieldValue.text, + selection = newTextFieldValue.selection + ) + updateMentions(result.first) + return newTextFieldValue.copy(selection = result.second) + } + + addedLength > 0 -> { + val result = + mentionAdjuster.adjustMentionsForInsertion( + mentions = mentions, + text = newTextFieldValue.text, + selection = newTextFieldValue.selection, + addedLength = addedLength + ) + updateMentions(result.first) + return newTextFieldValue.copy(selection = result.second) + } + } + + // To select the mention if the user clicks on it + val newSelection = if (oldTextFieldValue.text == newTextFieldValue.text) { + selectionManager.updateSelectionForMention( + oldTextFieldValue.selection, + newTextFieldValue.selection, + mentions + ) + } else { + newTextFieldValue.selection + } + + return newTextFieldValue.copy(selection = newSelection) + } +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/EnabledMessageComposer.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/EnabledMessageComposer.kt index 39fc2d15e6f..a9b15164782 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/EnabledMessageComposer.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/EnabledMessageComposer.kt @@ -69,9 +69,11 @@ import com.wire.android.ui.common.banner.SecurityClassificationBannerForConversa import com.wire.android.ui.common.bottombar.bottomNavigationBarHeight import com.wire.android.ui.common.colorsScheme import com.wire.android.ui.common.dimensions +import com.wire.android.ui.common.textfield.mention.MentionUpdateCoordinator import com.wire.android.ui.home.conversations.ConversationActionPermissionType import com.wire.android.ui.home.conversations.UsersTypingIndicatorForConversation import com.wire.android.ui.home.conversations.model.UriAsset +import com.wire.android.ui.home.messagecomposer.model.update import com.wire.android.ui.home.messagecomposer.state.AdditionalOptionSubMenuState import com.wire.android.ui.home.messagecomposer.state.InputType import com.wire.android.ui.home.messagecomposer.state.MessageComposerStateHolder @@ -206,10 +208,18 @@ fun EnabledMessageComposer( conversationId = conversationId, messageComposition = messageComposition.value, messageTextFieldValue = inputStateHolder.messageTextFieldValue, - onValueChange = { - inputStateHolder.messageTextFieldValue.value = it + onValueChange = { newTextField -> + val updatedTextField = MentionUpdateCoordinator().handle( + inputStateHolder.messageTextFieldValue.value, + newTextField, + messageComposition.value.selectedMentions, + updateMentions = { mentions -> + messageComposition.update { it.copy(selectedMentions = mentions) } + } + ) + inputStateHolder.messageTextFieldValue.value = updatedTextField }, - mentions = (messageComposition.value.selectedMentions), + mentions = messageComposition.value.selectedMentions, isTextExpanded = inputStateHolder.isTextExpanded, inputType = messageCompositionInputStateHolder.inputType, focusRequester = messageCompositionInputStateHolder.focusRequester, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/MessageComposerInput.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/MessageComposerInput.kt index e9c2b9012bf..1a4888f7f8c 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/MessageComposerInput.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/MessageComposerInput.kt @@ -189,7 +189,7 @@ private fun InputContent( viewModel: SelfDeletingMessageActionViewModel = hiltViewModelScoped( SelfDeletingMessageActionArgs(conversationId = conversationId) - ), + ) ) { ConstraintLayout(modifier = modifier) { val (additionalOptionButton, input, actions) = createRefs() 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 18059202bb4..28c6306fae6 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 @@ -112,7 +112,6 @@ class MessageCompositionHolder( .distinctUntilChanged() .collectLatest { (messageText, selection) -> updateTypingEvent(messageText) - updateMentionsIfNeeded(messageText) requestMentionSuggestionIfNeeded(messageText, selection) onSaveDraft(messageComposition.value.toDraft(messageText)) } @@ -125,10 +124,6 @@ class MessageCompositionHolder( } } - private fun updateMentionsIfNeeded(messageText: String) { - messageComposition.update { it.copy(selectedMentions = it.getSelectedMentions(messageText)) } - } - private fun requestMentionSuggestionIfNeeded(messageText: String, selection: TextRange) { if (selection.min != selection.max) { onClearMentionSearchResult() @@ -192,17 +187,24 @@ class MessageCompositionHolder( } fun addMention(contact: Contact) { - val mention = UIMention( + val mentionToAdd = UIMention( start = currentMentionStartIndex(messageTextFieldValue.value.text, messageTextFieldValue.value.selection), length = contact.name.length + 1, // +1 cause there is an "@" before it userId = UserId(contact.id, contact.domain), handler = String.MENTION_SYMBOL + contact.name ) - insertMentionIntoText(mention) + val updatedList = mutableListOf() + messageComposition.value.selectedMentions.forEach { mention -> + if (messageTextFieldValue.value.selection.start < mention.start) { + updatedList.add(mention.copy(start = mention.start + mentionToAdd.length )) + } else { + updatedList.add(mention) + } + } + updatedList.add(mentionToAdd) + insertMentionIntoText(mentionToAdd) messageComposition.update { - it.copy( - selectedMentions = it.selectedMentions.plus(mention).sortedBy { it.start } - ) + it.copy(selectedMentions = updatedList.sortedBy { it.start }) } } diff --git a/app/src/test/kotlin/com/wire/android/ui/common/textfield/MentionDeletionHandlerTest.kt b/app/src/test/kotlin/com/wire/android/ui/common/textfield/MentionDeletionHandlerTest.kt deleted file mode 100644 index f28ad9b2b00..00000000000 --- a/app/src/test/kotlin/com/wire/android/ui/common/textfield/MentionDeletionHandlerTest.kt +++ /dev/null @@ -1,97 +0,0 @@ -/* - * 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.ui.common.textfield - -import androidx.compose.ui.text.TextRange -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test - -class MentionDeletionHandlerTest { - - @Test - fun `given mention in text when deleting inside mention then mention is removed`() { - val oldText = "Hello @John Doe, how are you?" - val newText = "Hello , how are you?" - val oldSelection = TextRange(6, 17) - val mentions = listOf("@John Doe") - - val result = MentionDeletionHandler.handle(oldText, newText, oldSelection, mentions) - - assertEquals("Hello , how are you?", result) - } - - @Test - fun `given mention with last character deleted when deleting last character then mention is removed`() { - val oldText = "Hello @John Doe, how are you?" - val newText = "Hello @John Do, how are you?" - val oldSelection = TextRange(3, 13) - val mentions = listOf("@John Doe") - - val result = MentionDeletionHandler.handle(oldText, newText, oldSelection, mentions) - - assertEquals("Hello , how are you?", result) - } - - @Test - fun `given cursor at beginning of mention when no deletion then text remains unchanged`() { - val oldText = "Hello @John Doe, how are you?" - val newText = "Hello @John Doe, how are you?" - val oldSelection = TextRange(6, 6) - val mentions = listOf("@John Doe") - - val result = MentionDeletionHandler.handle(oldText, newText, oldSelection, mentions) - - assertEquals(oldText, result) - } - - @Test - fun `given text with mention when deleting outside of mention then text remains unchanged`() { - val oldText = "Hello @John Doe, how are you?" - val newText = "Hello @John Doehow are you?" - val oldSelection = TextRange(5, 6) - val mentions = listOf("@John Doe") - - val result = MentionDeletionHandler.handle(oldText, newText, oldSelection, mentions) - - assertEquals(newText, result) - } - - @Test - fun `given multiple mentions in text when deleting inside mentions then all mentions are removed`() { - val oldText = "Hello @John Doe and @Jane Doe, how are you?" - val newText = "Hello , how are you?" - val oldSelection = TextRange(6, 17) - val mentions = listOf("@John Doe", "@Jane Doe") - - val result = MentionDeletionHandler.handle(oldText, newText, oldSelection, mentions) - - assertEquals(newText, result) - } - - @Test - fun `given text without mentions when no mentions to delete then text remains unchanged`() { - val oldText = "Hello there, how are you?" - val newText = "Hello, how are you?" - val oldSelection = TextRange(6, 6) - val mentions = listOf("@John Doe") - - val result = MentionDeletionHandler.handle(oldText, newText, oldSelection, mentions) - - assertEquals(newText, result) - } -} diff --git a/app/src/test/kotlin/com/wire/android/ui/common/textfield/mention/MentionAdjusterTest.kt b/app/src/test/kotlin/com/wire/android/ui/common/textfield/mention/MentionAdjusterTest.kt new file mode 100644 index 00000000000..a7923e12e57 --- /dev/null +++ b/app/src/test/kotlin/com/wire/android/ui/common/textfield/mention/MentionAdjusterTest.kt @@ -0,0 +1,163 @@ +/* + * 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.ui.common.textfield.mention + +import androidx.compose.ui.text.TextRange +import com.wire.android.framework.TestUser +import com.wire.android.ui.home.conversations.model.UIMention +import org.junit.Assert.assertEquals +import org.junit.Test + +class MentionAdjusterTest { + + private val mentionAdjuster: MentionAdjuster = MentionAdjuster() + + // --- adjustMentionsForDeletion Tests --- + + @Test + fun `Given deleted text does not affect any mentions, When adjustMentionsForDeletion is called, Then mentions remain unchanged`() { + // Given + val mentions = listOf(UIMention(start = 6, length = 5, handler = "@user", userId = TestUser.USER_ID)) + val deletedLength = 2 + val text = "Hello @user again" + val selection = TextRange(13, 10) + + // When + val (updatedMentions, updatedSelection) = mentionAdjuster.adjustMentionsForDeletion( + mentions = mentions, + deletedLength = deletedLength, + text = text, + selection = selection + ) + + // Then + assertEquals(mentions, updatedMentions) + assertEquals(selection, updatedSelection) + } + + @Test + fun `Given deleted text affects mentions, When adjustMentionsForDeletion is called, Then mentions are adjusted and selection updated`() { + // Given + val mentions = listOf(UIMention(start = 12, length = 5, handler = "@user", userId = TestUser.USER_ID)) + val deletedLength = 2 // Simulate deleting 2 characters before the mention + val text = "Hello ain @user" + val selection = TextRange(7, 7) + + // When + val (updatedMentions, updatedSelection) = mentionAdjuster.adjustMentionsForDeletion( + mentions = mentions, + deletedLength = deletedLength, + text = text, + selection = selection + ) + + // Then + val expectedMention = UIMention(start = 10, length = 5, handler = "@user", userId = TestUser.USER_ID) + assertEquals(listOf(expectedMention), updatedMentions) + assertEquals(selection, updatedSelection) + } + + @Test + fun `Given cursor is at end of a mention, When adjustMentionsForDeletion is called, Then selection is updated to the mention's range`() { + // Given + val mentions = listOf(UIMention(start = 0, length = 5, handler = "@user", userId = TestUser.USER_ID)) + val deletedLength = 1 // Simulate deleting 1 character inside the mention + val text = "@user" + val selection = TextRange(5, 5) + + // When + val (updatedMentions, updatedSelection) = mentionAdjuster.adjustMentionsForDeletion( + mentions = mentions, + deletedLength = deletedLength, + text = text, + selection = selection + ) + + // Then + val expectedMention = UIMention(start = 0, length = 5, handler = "@user", userId = TestUser.USER_ID) + assertEquals(listOf(expectedMention), updatedMentions) + assertEquals(TextRange(0, 5), updatedSelection) + } + + // --- adjustMentionsForInsertion Tests --- + + @Test + fun `Given inserted text does not affect any mentions, When adjustMentionsForInsertion is called, Then mentions remain unchanged`() { + // Given + val mentions = listOf(UIMention(start = 5, length = 5, handler = "@user", userId = TestUser.USER_ID)) + val addedLength = 0 + val text = "Hello world" + val selection = TextRange(0, 5) + + // When + val (updatedMentions, updatedSelection) = mentionAdjuster.adjustMentionsForInsertion( + mentions = mentions, + text = text, + selection = selection, + addedLength = addedLength + ) + + // Then + assertEquals(mentions, updatedMentions) + assertEquals(selection, updatedSelection) + } + + @Test + fun `Given inserted text shifts mentions, When adjustMentionsForInsertion is called, Then mentions are adjusted`() { + // Given + val mentions = listOf(UIMention(start = 5, length = 5, handler = "@user", userId = TestUser.USER_ID)) + val addedLength = 2 // Simulate inserting 2 characters before the mention + val text = "Hello @user" + val selection = TextRange(0, 5) + + // When + val (updatedMentions, updatedSelection) = mentionAdjuster.adjustMentionsForInsertion( + mentions = mentions, + text = text, + selection = selection, + addedLength = addedLength + ) + + // Then + val expectedMention = UIMention(start = 7, length = 5, handler = "@user", userId = TestUser.USER_ID) + assertEquals(listOf(expectedMention), updatedMentions) + assertEquals(selection, updatedSelection) + } + + @Test + fun `Given inserted text shifts mentions, When adjustMentionsForInsertion is called, Then mentions are adjusted accordingly`() { + // Given + val mentions = listOf(UIMention(start = 0, length = 5, handler = "@user", userId = TestUser.USER_ID)) + val addedLength = 3 // Simulate inserting 3 characters before the mention + val text = "Hel world" + val selection = TextRange(0, 5) + + // When + val (updatedMentions, updatedSelection) = mentionAdjuster.adjustMentionsForInsertion( + mentions = mentions, + text = text, + selection = selection, + addedLength = addedLength + ) + + // Then + val expectedMention = UIMention(start = 3, length = 5, handler = "@user", userId = TestUser.USER_ID) + assertEquals(listOf(expectedMention), updatedMentions) + assertEquals(selection, updatedSelection) + } +} diff --git a/app/src/test/kotlin/com/wire/android/ui/common/textfield/mention/MentionSelectionManagerTest.kt b/app/src/test/kotlin/com/wire/android/ui/common/textfield/mention/MentionSelectionManagerTest.kt new file mode 100644 index 00000000000..26a7baee175 --- /dev/null +++ b/app/src/test/kotlin/com/wire/android/ui/common/textfield/mention/MentionSelectionManagerTest.kt @@ -0,0 +1,105 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.android.ui.common.textfield.mention + +import androidx.compose.ui.text.TextRange +import com.wire.android.framework.TestUser +import com.wire.android.ui.home.conversations.model.UIMention +import org.junit.Assert.assertEquals +import org.junit.Test + +class MentionSelectionManagerTest { + + private val selectionManager: MentionSelectionManager = MentionSelectionManager() + + @Test + fun `Given same old and new selection, When updateSelectionForMention is called, Then selection remains unchanged`() { + // Given + val oldSelection = TextRange(0, 5) + val newSelection = TextRange(0, 5) // Same as old selection + val mentions = listOf(UIMention(start = 0, length = 5, handler = "@user", userId = TestUser.USER_ID)) + + // When + val updatedSelection = selectionManager.updateSelectionForMention( + oldSelection = oldSelection, + newSelection = newSelection, + mentions = mentions + ) + + // Then + assertEquals(newSelection, updatedSelection) + } + + @Test + fun `Given new selection inside a mention, When updateSelectionForMention is called, Then selection updates to the mention's range`() { + // Given + val oldSelection = TextRange(0, 5) + val newSelection = TextRange(3, 3) // Inside the mention range + val mentions = listOf(UIMention(start = 0, length = 5, handler = "@user", userId = TestUser.USER_ID)) + + // When + val updatedSelection = selectionManager.updateSelectionForMention( + oldSelection = oldSelection, + newSelection = newSelection, + mentions = mentions + ) + + // Then + assertEquals(TextRange(0, 5), updatedSelection) + } + + @Test + fun `Given new selection outside of any mention, When updateSelectionForMention is called, Then selection remains unchanged`() { + // Given + val oldSelection = TextRange(0, 5) + val newSelection = TextRange(10, 10) // Outside the mention range + val mentions = listOf(UIMention(start = 0, length = 5, handler = "@user", userId = TestUser.USER_ID)) + + // When + val updatedSelection = selectionManager.updateSelectionForMention( + oldSelection = oldSelection, + newSelection = newSelection, + mentions = mentions + ) + + // Then + assertEquals(newSelection, updatedSelection) // Should remain unchanged + } + + @Test + fun `Given multiple mentions, When new selection is inside one of them, Then selection updates to the correct mention's range`() { + // Given + val oldSelection = TextRange(0, 5) + val newSelection = TextRange(8, 8) // Inside the second mention + val mentions = listOf( + UIMention(start = 0, length = 5, handler = "@user1", userId = TestUser.USER_ID), + UIMention(start = 6, length = 5, handler = "@user2", userId = TestUser.SELF_USER_ID), + UIMention(start = 15, length = 5, handler = "@user2", userId = TestUser.SELF_USER_ID) + ) + + // When + val updatedSelection = selectionManager.updateSelectionForMention( + oldSelection = oldSelection, + newSelection = newSelection, + mentions = mentions + ) + + // Then + assertEquals(TextRange(6, 11), updatedSelection) + } +} diff --git a/app/src/test/kotlin/com/wire/android/ui/common/textfield/mention/MentionUpdateCoordinatorTest.kt b/app/src/test/kotlin/com/wire/android/ui/common/textfield/mention/MentionUpdateCoordinatorTest.kt new file mode 100644 index 00000000000..ffaa13cdd9e --- /dev/null +++ b/app/src/test/kotlin/com/wire/android/ui/common/textfield/mention/MentionUpdateCoordinatorTest.kt @@ -0,0 +1,182 @@ +/* + * 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.ui.common.textfield.mention + +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.input.TextFieldValue +import com.wire.android.framework.TestUser +import com.wire.android.ui.home.conversations.model.UIMention +import com.wire.android.util.EMPTY +import io.mockk.MockKAnnotations +import io.mockk.every +import io.mockk.impl.annotations.MockK +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +class MentionUpdateCoordinatorTest { + + @MockK + private lateinit var mentionAdjuster: MentionAdjuster + + @MockK + private lateinit var selectionManager: MentionSelectionManager + + init { + MockKAnnotations.init(this) + } + + private val coordinator = MentionUpdateCoordinator( + mentionAdjuster = mentionAdjuster, + selectionManager = selectionManager + ) + + @Test + fun `Given empty new text, When handle is called, Then mentions should be cleared`() { + // Given + val oldTextFieldValue = TextFieldValue(text = "Hello", selection = TextRange(0, 5)) + val newTextFieldValue = TextFieldValue(text = "", selection = TextRange(0, 0)) + val mentions = listOf(UIMention(start = 0, length = 5, handler = "@user", userId = TestUser.USER_ID)) + var isInvoked = false + // When + val updatedTextFieldValue = coordinator.handle( + oldTextFieldValue, + newTextFieldValue, + mentions, + updateMentions = { isInvoked = true } + ) + + // Then + assertEquals(String.EMPTY, updatedTextFieldValue.text) + assertTrue(isInvoked) + } + + @Test + fun `Given no mention change, When handle is called, Then mentions should remain unchanged`() { + // Given + val oldTextFieldValue = TextFieldValue(text = "Hello", selection = TextRange(0, 5)) + val newTextFieldValue = TextFieldValue(text = "Hello", selection = TextRange(0, 5)) + val mentions = listOf(UIMention(start = 0, length = 5, handler = "@user", userId = TestUser.USER_ID)) + var isInvoked = false + every { + selectionManager.updateSelectionForMention(any(), any(), any()) + } returns TextRange(0, 5) + + // When + val updatedTextFieldValue = coordinator.handle( + oldTextFieldValue, + newTextFieldValue, + mentions, + updateMentions = { isInvoked = true } + ) + + // Then + assertEquals("Hello", updatedTextFieldValue.text) + assertEquals(TextRange(0, 5), updatedTextFieldValue.selection) + assertFalse(isInvoked) + } + + @Test + fun `Given text deletion, When handle is called, Then mentions and selection should adjust`() { + // Given + val oldTextFieldValue = TextFieldValue(text = "Hello @user", selection = TextRange(0, 11)) + val newTextFieldValue = TextFieldValue(text = "Hello", selection = TextRange(0, 5)) + val mentions = listOf(UIMention(start = 6, length = 5, handler = "@user", userId = TestUser.USER_ID)) + var isInvoked = false + + every { + mentionAdjuster.adjustMentionsForDeletion( + mentions = mentions, + deletedLength = 6, + text = "Hello", + selection = newTextFieldValue.selection + ) + } returns Pair(listOf(UIMention(start = 6, length = 5, handler = "@user", userId = TestUser.USER_ID)), TextRange(0, 5)) + + // When + val updatedTextFieldValue = coordinator.handle( + oldTextFieldValue, + newTextFieldValue, + mentions, + updateMentions = { isInvoked = true } + ) + + // Then + assertEquals("Hello", updatedTextFieldValue.text) + assertEquals(TextRange(0, 5), updatedTextFieldValue.selection) + assertTrue(isInvoked) + } + + @Test + fun `Given text insertion, When handle is called, Then mentions should shift accordingly`() { + // Given + val oldTextFieldValue = TextFieldValue(text = "Hello", selection = TextRange(0, 5)) + val newTextFieldValue = TextFieldValue(text = "Hello @user", selection = TextRange(0, 5)) + val mentions = listOf(UIMention(start = 6, length = 5, handler = "@user", userId = TestUser.USER_ID)) + var isInvoked = false + + every { + mentionAdjuster.adjustMentionsForInsertion( + mentions = mentions, + addedLength = 6, + text = any(), + selection = any() + ) + } returns Pair(listOf(UIMention(start = 12, length = 5, handler = "@user", userId = TestUser.USER_ID)), TextRange(0, 5)) + + // When + val updatedTextFieldValue = coordinator.handle( + oldTextFieldValue, + newTextFieldValue, + mentions, + updateMentions = { isInvoked = true } + ) + + // Then + assertEquals("Hello @user", updatedTextFieldValue.text) + assertEquals(TextRange(0, 5), updatedTextFieldValue.selection) + assertTrue(isInvoked) + } + + @Test + fun `Given selection inside mention, When handle is called, Then selection should update`() { + // Given + val oldTextFieldValue = TextFieldValue(text = "Hello @user", selection = TextRange(0, 5)) + val newTextFieldValue = TextFieldValue(text = "Hello @user", selection = TextRange(0, 5)) + val mentions = listOf(UIMention(start = 6, length = 5, handler = "@user", userId = TestUser.USER_ID)) + var isInvoked = false + + every { + selectionManager.updateSelectionForMention(any(), any(), any()) + } returns TextRange(6, 11) + + + // When + val updatedTextFieldValue = coordinator.handle( + oldTextFieldValue, + newTextFieldValue, + mentions, + updateMentions = { isInvoked = true } + ) + + // Then + assertEquals(TextRange(6, 11), updatedTextFieldValue.selection) + assertFalse(isInvoked) + } +} From 4680d96eb346882ecc7932be9513e9ae681c060e Mon Sep 17 00:00:00 2001 From: ohassine Date: Mon, 2 Dec 2024 10:04:05 +0100 Subject: [PATCH 15/18] feat: detekt --- .../ui/common/textfield/mention/MentionAdjuster.kt | 1 + .../common/textfield/mention/MentionUpdateCoordinator.kt | 1 + .../messagecomposer/state/MessageCompositionHolder.kt | 2 +- .../ui/common/textfield/mention/MentionAdjusterTest.kt | 8 ++++---- .../textfield/mention/MentionSelectionManagerTest.kt | 6 +++--- .../textfield/mention/MentionUpdateCoordinatorTest.kt | 1 - kalium | 2 +- 7 files changed, 11 insertions(+), 10 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/common/textfield/mention/MentionAdjuster.kt b/app/src/main/kotlin/com/wire/android/ui/common/textfield/mention/MentionAdjuster.kt index db29d619562..bf220943e78 100644 --- a/app/src/main/kotlin/com/wire/android/ui/common/textfield/mention/MentionAdjuster.kt +++ b/app/src/main/kotlin/com/wire/android/ui/common/textfield/mention/MentionAdjuster.kt @@ -32,6 +32,7 @@ class MentionAdjuster { * @param text The new text after deletion. * @param selection The current selection. */ + @Suppress("NestedBlockDepth") fun adjustMentionsForDeletion( mentions: List, deletedLength: Int, diff --git a/app/src/main/kotlin/com/wire/android/ui/common/textfield/mention/MentionUpdateCoordinator.kt b/app/src/main/kotlin/com/wire/android/ui/common/textfield/mention/MentionUpdateCoordinator.kt index b5f57c4bec6..b4a1c883f46 100644 --- a/app/src/main/kotlin/com/wire/android/ui/common/textfield/mention/MentionUpdateCoordinator.kt +++ b/app/src/main/kotlin/com/wire/android/ui/common/textfield/mention/MentionUpdateCoordinator.kt @@ -27,6 +27,7 @@ class MentionUpdateCoordinator( private val mentionAdjuster: MentionAdjuster = MentionAdjuster(), private val selectionManager: MentionSelectionManager = MentionSelectionManager() ) { + @Suppress("ReturnCount") fun handle( oldTextFieldValue: TextFieldValue, newTextFieldValue: TextFieldValue, 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 28c6306fae6..b7e22716c3d 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 @@ -196,7 +196,7 @@ class MessageCompositionHolder( val updatedList = mutableListOf() messageComposition.value.selectedMentions.forEach { mention -> if (messageTextFieldValue.value.selection.start < mention.start) { - updatedList.add(mention.copy(start = mention.start + mentionToAdd.length )) + updatedList.add(mention.copy(start = mention.start + mentionToAdd.length)) } else { updatedList.add(mention) } diff --git a/app/src/test/kotlin/com/wire/android/ui/common/textfield/mention/MentionAdjusterTest.kt b/app/src/test/kotlin/com/wire/android/ui/common/textfield/mention/MentionAdjusterTest.kt index a7923e12e57..abc2c8b43db 100644 --- a/app/src/test/kotlin/com/wire/android/ui/common/textfield/mention/MentionAdjusterTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/common/textfield/mention/MentionAdjusterTest.kt @@ -54,7 +54,7 @@ class MentionAdjusterTest { fun `Given deleted text affects mentions, When adjustMentionsForDeletion is called, Then mentions are adjusted and selection updated`() { // Given val mentions = listOf(UIMention(start = 12, length = 5, handler = "@user", userId = TestUser.USER_ID)) - val deletedLength = 2 // Simulate deleting 2 characters before the mention + val deletedLength = 2 // Simulate deleting 2 characters before the mention val text = "Hello ain @user" val selection = TextRange(7, 7) @@ -76,7 +76,7 @@ class MentionAdjusterTest { fun `Given cursor is at end of a mention, When adjustMentionsForDeletion is called, Then selection is updated to the mention's range`() { // Given val mentions = listOf(UIMention(start = 0, length = 5, handler = "@user", userId = TestUser.USER_ID)) - val deletedLength = 1 // Simulate deleting 1 character inside the mention + val deletedLength = 1 // Simulate deleting 1 character inside the mention val text = "@user" val selection = TextRange(5, 5) @@ -121,7 +121,7 @@ class MentionAdjusterTest { fun `Given inserted text shifts mentions, When adjustMentionsForInsertion is called, Then mentions are adjusted`() { // Given val mentions = listOf(UIMention(start = 5, length = 5, handler = "@user", userId = TestUser.USER_ID)) - val addedLength = 2 // Simulate inserting 2 characters before the mention + val addedLength = 2 // Simulate inserting 2 characters before the mention val text = "Hello @user" val selection = TextRange(0, 5) @@ -143,7 +143,7 @@ class MentionAdjusterTest { fun `Given inserted text shifts mentions, When adjustMentionsForInsertion is called, Then mentions are adjusted accordingly`() { // Given val mentions = listOf(UIMention(start = 0, length = 5, handler = "@user", userId = TestUser.USER_ID)) - val addedLength = 3 // Simulate inserting 3 characters before the mention + val addedLength = 3 // Simulate inserting 3 characters before the mention val text = "Hel world" val selection = TextRange(0, 5) diff --git a/app/src/test/kotlin/com/wire/android/ui/common/textfield/mention/MentionSelectionManagerTest.kt b/app/src/test/kotlin/com/wire/android/ui/common/textfield/mention/MentionSelectionManagerTest.kt index 26a7baee175..8daf2c2e19c 100644 --- a/app/src/test/kotlin/com/wire/android/ui/common/textfield/mention/MentionSelectionManagerTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/common/textfield/mention/MentionSelectionManagerTest.kt @@ -67,7 +67,7 @@ class MentionSelectionManagerTest { fun `Given new selection outside of any mention, When updateSelectionForMention is called, Then selection remains unchanged`() { // Given val oldSelection = TextRange(0, 5) - val newSelection = TextRange(10, 10) // Outside the mention range + val newSelection = TextRange(10, 10) // Outside the mention range val mentions = listOf(UIMention(start = 0, length = 5, handler = "@user", userId = TestUser.USER_ID)) // When @@ -78,14 +78,14 @@ class MentionSelectionManagerTest { ) // Then - assertEquals(newSelection, updatedSelection) // Should remain unchanged + assertEquals(newSelection, updatedSelection) // Should remain unchanged } @Test fun `Given multiple mentions, When new selection is inside one of them, Then selection updates to the correct mention's range`() { // Given val oldSelection = TextRange(0, 5) - val newSelection = TextRange(8, 8) // Inside the second mention + val newSelection = TextRange(8, 8) // Inside the second mention val mentions = listOf( UIMention(start = 0, length = 5, handler = "@user1", userId = TestUser.USER_ID), UIMention(start = 6, length = 5, handler = "@user2", userId = TestUser.SELF_USER_ID), diff --git a/app/src/test/kotlin/com/wire/android/ui/common/textfield/mention/MentionUpdateCoordinatorTest.kt b/app/src/test/kotlin/com/wire/android/ui/common/textfield/mention/MentionUpdateCoordinatorTest.kt index ffaa13cdd9e..94dd56d4ece 100644 --- a/app/src/test/kotlin/com/wire/android/ui/common/textfield/mention/MentionUpdateCoordinatorTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/common/textfield/mention/MentionUpdateCoordinatorTest.kt @@ -166,7 +166,6 @@ class MentionUpdateCoordinatorTest { selectionManager.updateSelectionForMention(any(), any(), any()) } returns TextRange(6, 11) - // When val updatedTextFieldValue = coordinator.handle( oldTextFieldValue, diff --git a/kalium b/kalium index 4c476f7390e..786a7b83e84 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 4c476f7390ed6a7ffe27bc9ad0ce649c532ac35b +Subproject commit 786a7b83e84663909c6c40b3f6c72263fe935661 From 1e111088ca55666242c705b030174b8e7e7142a2 Mon Sep 17 00:00:00 2001 From: ohassine Date: Mon, 2 Dec 2024 10:14:17 +0100 Subject: [PATCH 16/18] feat: kalium reference --- kalium | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kalium b/kalium index 786a7b83e84..ee39af712e6 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 786a7b83e84663909c6c40b3f6c72263fe935661 +Subproject commit ee39af712e6caebe0b5539c85fa076f3b7446069 From 45177828b7e64993bf51a83a68805c2e7594adc1 Mon Sep 17 00:00:00 2001 From: ohassine Date: Mon, 2 Dec 2024 10:42:22 +0100 Subject: [PATCH 17/18] feat: kalium reference --- kalium | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kalium b/kalium index ee39af712e6..b391c817503 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit ee39af712e6caebe0b5539c85fa076f3b7446069 +Subproject commit b391c8175037b57766789f353452bd45abe364a5 From 5ebe739cdaa35df03e69b5cfbd59d2b076467388 Mon Sep 17 00:00:00 2001 From: ohassine Date: Mon, 2 Dec 2024 11:55:48 +0100 Subject: [PATCH 18/18] chore: documentation --- .../common/textfield/mention/MentionSelectionManager.kt | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/src/main/kotlin/com/wire/android/ui/common/textfield/mention/MentionSelectionManager.kt b/app/src/main/kotlin/com/wire/android/ui/common/textfield/mention/MentionSelectionManager.kt index d6c2f299ca3..eb03e606906 100644 --- a/app/src/main/kotlin/com/wire/android/ui/common/textfield/mention/MentionSelectionManager.kt +++ b/app/src/main/kotlin/com/wire/android/ui/common/textfield/mention/MentionSelectionManager.kt @@ -24,6 +24,15 @@ import com.wire.android.ui.home.conversations.model.UIMention * Manages the selection for mentions. */ class MentionSelectionManager { + + /** + * if the selection is inside the mention's range, it will return the mention's range. + * Otherwise, it will return the new selection. + * @param oldSelection the old selection. + * @param newSelection the new selection. + * @param mentions the list of mentions. + * @return the new selection. + */ fun updateSelectionForMention( oldSelection: TextRange, newSelection: TextRange,