From 7e2785cd14513a4a2acbdaeefb04355f7b9f25a3 Mon Sep 17 00:00:00 2001 From: Jakub Zerko Date: Thu, 21 Nov 2024 14:13:56 +0100 Subject: [PATCH] feat: add and remove conversation favorite --- .../di/accountScoped/ConversationModule.kt | 1 - .../com/wire/android/model/SnackBarMessage.kt | 7 + .../conversation/ConversationSheetContent.kt | 6 +- .../conversation/HomeSheetContent.kt | 28 +++- .../ChangeConversationFavoriteStateArgs.kt | 26 +++ .../folder/ChangeConversationFavoriteVM.kt | 71 ++++++++ .../details/GroupConversationDetailsScreen.kt | 12 +- .../GroupConversationDetailsViewModel.kt | 14 -- ...ersationDetailsBottomSheetEventsHandler.kt | 2 - .../ConversationListViewModel.kt | 15 -- .../ConversationsScreenContent.kt | 13 +- .../other/OtherUserProfileEventsHandlers.kt | 2 - .../other/OtherUserProfileScreen.kt | 15 +- .../other/OtherUserProfileScreenViewModel.kt | 17 -- .../OtherUserProfileBottomSheet.kt | 4 +- .../android/util/ui/SnackBarMessageHandler.kt | 4 +- app/src/main/res/values/strings.xml | 5 + .../android/framework/TestConversationItem.kt | 6 +- .../ConversationSheetContentTest.kt | 9 +- .../ChangeConversationFavoriteVMTest.kt | 152 ++++++++++++++++++ .../GroupConversationDetailsViewModelTest.kt | 1 + kalium | 2 +- 22 files changed, 341 insertions(+), 71 deletions(-) create mode 100644 app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/folder/ChangeConversationFavoriteStateArgs.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/folder/ChangeConversationFavoriteVM.kt create mode 100644 app/src/test/kotlin/com/wire/android/ui/common/bottomsheet/folder/ChangeConversationFavoriteVMTest.kt diff --git a/app/src/main/kotlin/com/wire/android/di/accountScoped/ConversationModule.kt b/app/src/main/kotlin/com/wire/android/di/accountScoped/ConversationModule.kt index ed2a20aa86..f726d098dc 100644 --- a/app/src/main/kotlin/com/wire/android/di/accountScoped/ConversationModule.kt +++ b/app/src/main/kotlin/com/wire/android/di/accountScoped/ConversationModule.kt @@ -337,5 +337,4 @@ class ConversationModule { @Provides fun provideRemoveConversationFromFavoritesUseCase(conversationScope: ConversationScope) = conversationScope.removeConversationFromFavorites - } diff --git a/app/src/main/kotlin/com/wire/android/model/SnackBarMessage.kt b/app/src/main/kotlin/com/wire/android/model/SnackBarMessage.kt index 8a020d7879..a3fb5b9700 100644 --- a/app/src/main/kotlin/com/wire/android/model/SnackBarMessage.kt +++ b/app/src/main/kotlin/com/wire/android/model/SnackBarMessage.kt @@ -27,3 +27,10 @@ interface SnackBarMessage { val uiText: UIText val actionLabel: UIText? get() = null } + +data class DefaultSnackBarMessage( + override val uiText: UIText, + override val actionLabel: UIText? = null +) : SnackBarMessage + +fun UIText.asSnackBarMessage(actionLabel: UIText? = null): SnackBarMessage = DefaultSnackBarMessage(this, actionLabel) diff --git a/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/conversation/ConversationSheetContent.kt b/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/conversation/ConversationSheetContent.kt index 2b3ae8de03..f7aeeec65a 100644 --- a/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/conversation/ConversationSheetContent.kt +++ b/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/conversation/ConversationSheetContent.kt @@ -37,7 +37,7 @@ import com.wire.kalium.logic.data.user.UserId fun ConversationSheetContent( conversationSheetState: ConversationSheetState, onMutingConversationStatusChange: () -> Unit, - changeFavoriteState: (conversationId: ConversationId, isFavorite: Boolean) -> Unit, + changeFavoriteState: (GroupDialogState, addToFavorite: Boolean) -> Unit, moveConversationToFolder: () -> Unit, updateConversationArchiveStatus: (DialogState) -> Unit, clearConversationContent: (DialogState) -> Unit, @@ -148,8 +148,8 @@ data class ConversationSheetContent( conversationTypeDetail is ConversationTypeDetail.Private && conversationTypeDetail.blockingState == BlockingState.BLOCKED fun canAddToFavourite(): Boolean = isFavorite != null && - ((conversationTypeDetail is ConversationTypeDetail.Private && conversationTypeDetail.blockingState != BlockingState.BLOCKED) - || conversationTypeDetail is ConversationTypeDetail.Group) + ((conversationTypeDetail is ConversationTypeDetail.Private && conversationTypeDetail.blockingState != BlockingState.BLOCKED) + || conversationTypeDetail is ConversationTypeDetail.Group) fun isAbandonedOneOnOneConversation(participantsCount: Int): Boolean = title.isEmpty() && participantsCount == 1 } diff --git a/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/conversation/HomeSheetContent.kt b/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/conversation/HomeSheetContent.kt index 4a19a852cf..59c87eed04 100644 --- a/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/conversation/HomeSheetContent.kt +++ b/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/conversation/HomeSheetContent.kt @@ -51,14 +51,15 @@ import com.wire.android.ui.home.conversationslist.model.getMutedStatusTextResour import com.wire.android.ui.theme.wireColorScheme import com.wire.android.ui.theme.wireTypography import com.wire.kalium.logic.data.conversation.MutedConversationStatus -import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.user.ConnectionState +// items cannot be simplified +@Suppress("CyclomaticComplexMethod") @Composable internal fun ConversationMainSheetContent( conversationSheetContent: ConversationSheetContent, // TODO(profile): enable when implemented - changeFavoriteState: (conversationId: ConversationId, isFavorite: Boolean) -> Unit, + changeFavoriteState: (dialogState: GroupDialogState, addToFavorite: Boolean) -> Unit, // moveConversationToFolder: () -> Unit, updateConversationArchiveStatus: (DialogState) -> Unit, clearConversationContent: (DialogState) -> Unit, @@ -110,7 +111,7 @@ internal fun ConversationMainSheetContent( } } - if (conversationSheetContent.canAddToFavourite() && !conversationSheetContent.isArchived) + if (conversationSheetContent.canAddToFavourite() && !conversationSheetContent.isArchived) { conversationSheetContent.isFavorite?.let { isFavorite -> if (isFavorite) { add { @@ -122,7 +123,15 @@ internal fun ConversationMainSheetContent( contentDescription = stringResource(R.string.content_description_remove_from_favourites), ) }, - onItemClick = { changeFavoriteState(conversationSheetContent.conversationId, true) } + onItemClick = { + changeFavoriteState( + GroupDialogState( + conversationSheetContent.conversationId, + conversationSheetContent.title + ), + false + ) + } ) } } else { @@ -135,11 +144,20 @@ internal fun ConversationMainSheetContent( contentDescription = stringResource(R.string.content_description_add_to_favourite), ) }, - onItemClick = { changeFavoriteState(conversationSheetContent.conversationId, false) } + onItemClick = { + changeFavoriteState( + GroupDialogState( + conversationSheetContent.conversationId, + conversationSheetContent.title + ), + true + ) + } ) } } } + } // TODO(profile): enable when implemented // add { // MenuBottomSheetItem( diff --git a/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/folder/ChangeConversationFavoriteStateArgs.kt b/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/folder/ChangeConversationFavoriteStateArgs.kt new file mode 100644 index 0000000000..a3dd9b6b5b --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/folder/ChangeConversationFavoriteStateArgs.kt @@ -0,0 +1,26 @@ +/* + * 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.bottomsheet.folder + +import com.wire.android.di.ScopedArgs +import kotlinx.serialization.Serializable + +@Serializable +object ChangeConversationFavoriteStateArgs : ScopedArgs { + override val key = "ConnectionActionButtonArgsKey" +} diff --git a/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/folder/ChangeConversationFavoriteVM.kt b/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/folder/ChangeConversationFavoriteVM.kt new file mode 100644 index 0000000000..6d2c0a14cd --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/folder/ChangeConversationFavoriteVM.kt @@ -0,0 +1,71 @@ +/* + * 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.bottomsheet.folder + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.wire.android.R +import com.wire.android.di.ViewModelScopedPreview +import com.wire.android.model.SnackBarMessage +import com.wire.android.model.asSnackBarMessage +import com.wire.android.ui.home.conversationslist.model.GroupDialogState +import com.wire.android.util.ui.UIText +import com.wire.kalium.logic.feature.conversation.folder.AddConversationToFavoritesUseCase +import com.wire.kalium.logic.feature.conversation.folder.RemoveConversationFromFavoritesUseCase +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@ViewModelScopedPreview +interface ChangeConversationFavoriteVM { + val infoMessage: SharedFlow + get() = MutableSharedFlow() + + fun changeFavoriteState(dialogState: GroupDialogState, addToFavorite: Boolean) {} +} + +@HiltViewModel +class ChangeConversationFavoriteVMImpl @Inject constructor( + private val addConversationToFavorites: AddConversationToFavoritesUseCase, + private val removeConversationFromFavorites: RemoveConversationFromFavoritesUseCase, +) : ChangeConversationFavoriteVM, ViewModel() { + + private val _infoMessage = MutableSharedFlow() + override val infoMessage = _infoMessage.asSharedFlow() + + override fun changeFavoriteState(dialogState: GroupDialogState, addToFavorite: Boolean) { + viewModelScope.launch { + val messageResource = if (addToFavorite) { + when (addConversationToFavorites(dialogState.conversationId)) { + is AddConversationToFavoritesUseCase.Result.Failure -> R.string.error_adding_to_favorite + AddConversationToFavoritesUseCase.Result.Success -> R.string.success_adding_to_favorite + } + } else { + when (removeConversationFromFavorites(dialogState.conversationId)) { + is RemoveConversationFromFavoritesUseCase.Result.Failure -> R.string.error_removing_from_favorite + RemoveConversationFromFavoritesUseCase.Result.Success -> R.string.success_removing_from_favorite + } + } + _infoMessage.emit(UIText.StringResource(messageResource, dialogState.conversationName).asSnackBarMessage()) + } + } +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsScreen.kt index a061fba9f0..b067f167ae 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsScreen.kt @@ -63,6 +63,7 @@ import com.ramcosta.composedestinations.result.ResultBackNavigator import com.ramcosta.composedestinations.result.ResultRecipient import com.wire.android.R import com.wire.android.appLogger +import com.wire.android.di.hiltViewModelScoped import com.wire.android.navigation.NavigationCommand import com.wire.android.navigation.Navigator import com.wire.android.navigation.WireDestination @@ -78,6 +79,9 @@ import com.wire.android.ui.common.bottomsheet.WireModalSheetLayout import com.wire.android.ui.common.bottomsheet.conversation.ConversationSheetContent import com.wire.android.ui.common.bottomsheet.conversation.ConversationTypeDetail import com.wire.android.ui.common.bottomsheet.conversation.rememberConversationSheetState +import com.wire.android.ui.common.bottomsheet.folder.ChangeConversationFavoriteStateArgs +import com.wire.android.ui.common.bottomsheet.folder.ChangeConversationFavoriteVM +import com.wire.android.ui.common.bottomsheet.folder.ChangeConversationFavoriteVMImpl import com.wire.android.ui.common.bottomsheet.rememberWireModalSheetState import com.wire.android.ui.common.bottomsheet.show import com.wire.android.ui.common.button.WirePrimaryButton @@ -285,7 +289,11 @@ private fun GroupConversationDetailsContent( isLoading: Boolean, isAbandonedOneOnOneConversation: Boolean, onSearchConversationMessagesClick: () -> Unit, - onConversationMediaClick: () -> Unit + onConversationMediaClick: () -> Unit, + changeConversationFavoriteStateViewModel: ChangeConversationFavoriteVM = + hiltViewModelScoped( + ChangeConversationFavoriteStateArgs + ), ) { val scope = rememberCoroutineScope() val resources = LocalContext.current.resources @@ -461,7 +469,7 @@ private fun GroupConversationDetailsContent( ) } }, - changeFavoriteState = bottomSheetEventsHandler::changeFavoriteState, + changeFavoriteState = changeConversationFavoriteStateViewModel::changeFavoriteState, moveConversationToFolder = bottomSheetEventsHandler::onMoveConversationToFolder, updateConversationArchiveStatus = { // Only show the confirmation dialog if the conversation is not archived diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsViewModel.kt index 16253be0ba..e5704c3209 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsViewModel.kt @@ -56,8 +56,6 @@ import com.wire.kalium.logic.feature.conversation.UpdateConversationAccessRoleUs import com.wire.kalium.logic.feature.conversation.UpdateConversationArchivedStatusUseCase import com.wire.kalium.logic.feature.conversation.UpdateConversationMutedStatusUseCase import com.wire.kalium.logic.feature.conversation.UpdateConversationReceiptModeUseCase -import com.wire.kalium.logic.feature.conversation.folder.AddConversationToFavoritesUseCase -import com.wire.kalium.logic.feature.conversation.folder.RemoveConversationFromFavoritesUseCase import com.wire.kalium.logic.feature.publicuser.RefreshUsersWithoutMetadataUseCase import com.wire.kalium.logic.feature.selfDeletingMessages.ObserveSelfDeletionTimerSettingsForConversationUseCase import com.wire.kalium.logic.feature.team.DeleteTeamConversationUseCase @@ -102,8 +100,6 @@ class GroupConversationDetailsViewModel @Inject constructor( override val savedStateHandle: SavedStateHandle, private val isMLSEnabled: IsMLSEnabledUseCase, private val getDefaultProtocol: GetDefaultProtocolUseCase, - private val addConversationToFavorites: AddConversationToFavoritesUseCase, - private val removeConversationFromFavorites: RemoveConversationFromFavoritesUseCase, refreshUsersWithoutMetadata: RefreshUsersWithoutMetadataUseCase, ) : GroupConversationParticipantsViewModel( savedStateHandle, observeConversationMembers, refreshUsersWithoutMetadata @@ -379,16 +375,6 @@ class GroupConversationDetailsViewModel @Inject constructor( } } - override fun changeFavoriteState(conversationId: ConversationId, isFavorite: Boolean) { - viewModelScope.launch { - if (isFavorite) { - removeConversationFromFavorites(conversationId) - } else { - addConversationToFavorites(conversationId) - } - } - } - @Suppress("EmptyFunctionBlock") override fun onMoveConversationToFolder(conversationId: ConversationId?) { } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/menu/GroupConversationDetailsBottomSheetEventsHandler.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/menu/GroupConversationDetailsBottomSheetEventsHandler.kt index c98f66923a..6a0d8132b1 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/menu/GroupConversationDetailsBottomSheetEventsHandler.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/menu/GroupConversationDetailsBottomSheetEventsHandler.kt @@ -27,7 +27,6 @@ import com.wire.kalium.util.DateTimeUtil @Suppress("TooManyFunctions") interface GroupConversationDetailsBottomSheetEventsHandler { fun onMutingConversationStatusChange(conversationId: ConversationId?, status: MutedConversationStatus, onMessage: (UIText) -> Unit) - fun changeFavoriteState(conversationId: ConversationId, isFavorite: Boolean) fun onMoveConversationToFolder(conversationId: ConversationId? = null) fun updateConversationArchiveStatus( dialogState: DialogState, @@ -47,7 +46,6 @@ interface GroupConversationDetailsBottomSheetEventsHandler { ) { } - override fun changeFavoriteState(conversationId: ConversationId, isFavorite: Boolean) {} override fun onMoveConversationToFolder(conversationId: ConversationId?) {} override fun updateConversationArchiveStatus( dialogState: DialogState, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModel.kt index 5f30156ccf..851ab3adec 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModel.kt @@ -64,8 +64,6 @@ import com.wire.kalium.logic.feature.conversation.RefreshConversationsWithoutMet import com.wire.kalium.logic.feature.conversation.RemoveMemberFromConversationUseCase import com.wire.kalium.logic.feature.conversation.UpdateConversationArchivedStatusUseCase import com.wire.kalium.logic.feature.conversation.UpdateConversationMutedStatusUseCase -import com.wire.kalium.logic.feature.conversation.folder.AddConversationToFavoritesUseCase -import com.wire.kalium.logic.feature.conversation.folder.RemoveConversationFromFavoritesUseCase import com.wire.kalium.logic.feature.publicuser.RefreshUsersWithoutMetadataUseCase import com.wire.kalium.logic.feature.team.DeleteTeamConversationUseCase import com.wire.kalium.logic.feature.team.Result @@ -108,7 +106,6 @@ interface ConversationListViewModel { fun leaveGroup(leaveGroupState: GroupDialogState) {} fun clearConversationContent(dialogState: DialogState) {} fun muteConversation(conversationId: ConversationId?, mutedConversationStatus: MutedConversationStatus) {} - fun changeConversationFavoriteState(conversationId: ConversationId, isFavorite: Boolean) {} fun moveConversationToFolder() {} fun searchQueryChanged(searchQuery: String) {} } @@ -135,8 +132,6 @@ class ConversationListViewModelImpl @AssistedInject constructor( private val refreshUsersWithoutMetadata: RefreshUsersWithoutMetadataUseCase, private val refreshConversationsWithoutMetadata: RefreshConversationsWithoutMetadataUseCase, private val updateConversationArchivedStatus: UpdateConversationArchivedStatusUseCase, - private val addConversationToFavorites: AddConversationToFavoritesUseCase, - private val removeConversationFromFavorites: RemoveConversationFromFavoritesUseCase, @CurrentAccount val currentAccount: UserId, private val wireSessionImageLoader: WireSessionImageLoader, private val userTypeMapper: UserTypeMapper, @@ -358,16 +353,6 @@ class ConversationListViewModelImpl @AssistedInject constructor( } } - override fun changeConversationFavoriteState(conversationId: ConversationId, isFavorite: Boolean) { - viewModelScope.launch { - if(isFavorite) { - removeConversationFromFavorites(conversationId) - } else { - addConversationToFavorites(conversationId) - } - } - } - // TODO: needs to be implemented @Suppress("EmptyFunctionBlock") override fun moveConversationToFolder() { diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationsScreenContent.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationsScreenContent.kt index 7735e3c4ad..244fc83706 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationsScreenContent.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationsScreenContent.kt @@ -33,6 +33,7 @@ import androidx.paging.LoadState import androidx.paging.compose.collectAsLazyPagingItems import com.wire.android.R import com.wire.android.appLogger +import com.wire.android.di.hiltViewModelScoped import com.wire.android.feature.analytics.AnonymousAnalyticsManagerImpl import com.wire.android.feature.analytics.model.AnalyticsEvent import com.wire.android.navigation.NavigationCommand @@ -44,6 +45,9 @@ import com.wire.android.ui.common.bottomsheet.WireModalSheetLayout import com.wire.android.ui.common.bottomsheet.conversation.ConversationOptionNavigation import com.wire.android.ui.common.bottomsheet.conversation.ConversationSheetContent import com.wire.android.ui.common.bottomsheet.conversation.rememberConversationSheetState +import com.wire.android.ui.common.bottomsheet.folder.ChangeConversationFavoriteStateArgs +import com.wire.android.ui.common.bottomsheet.folder.ChangeConversationFavoriteVM +import com.wire.android.ui.common.bottomsheet.folder.ChangeConversationFavoriteVMImpl import com.wire.android.ui.common.bottomsheet.rememberWireModalSheetState import com.wire.android.ui.common.dialogs.ArchiveConversationDialog import com.wire.android.ui.common.dialogs.BlockUserDialogContent @@ -98,6 +102,10 @@ fun ConversationsScreenContent( LocalInspectionMode.current -> ConversationCallListViewModelPreview else -> hiltViewModel(key = "call_${conversationsSource.name}") }, + changeConversationFavoriteStateViewModel: ChangeConversationFavoriteVM = + hiltViewModelScoped( + ChangeConversationFavoriteStateArgs + ), ) { var currentConversationOptionNavigation by remember { mutableStateOf(ConversationOptionNavigation.Home) @@ -304,7 +312,7 @@ fun ConversationsScreenContent( mutedConversationStatus = conversationState.conversationSheetContent!!.mutingConversationState ) }, - changeFavoriteState = conversationListViewModel::changeConversationFavoriteState, + changeFavoriteState = changeConversationFavoriteStateViewModel::changeFavoriteState, moveConversationToFolder = conversationListViewModel::moveConversationToFolder, updateConversationArchiveStatus = showConfirmationDialogOrUnarchive(), clearConversationContent = clearContentDialogState::show, @@ -318,6 +326,9 @@ fun ConversationsScreenContent( } SnackBarMessageHandler(infoMessages = conversationListViewModel.infoMessage) + SnackBarMessageHandler(infoMessages = changeConversationFavoriteStateViewModel.infoMessage, onEmitted = { + sheetState.hide() + }) } private const val TAG = "BaseConversationsScreen" diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileEventsHandlers.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileEventsHandlers.kt index bbba716a03..162b14cee3 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileEventsHandlers.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileEventsHandlers.kt @@ -67,7 +67,6 @@ interface OtherUserProfileFooterEventsHandler { interface OtherUserProfileBottomSheetEventsHandler { fun onChangeMemberRole(role: Conversation.Member.Role) fun onMutingConversationStatusChange(conversationId: ConversationId?, status: MutedConversationStatus) - fun onChangeFavoriteState(conversationId: ConversationId? = null, isFavorite: Boolean) fun onMoveConversationToFolder(conversationId: ConversationId? = null) fun onMoveConversationToArchive(dialogState: DialogState) fun onClearConversationContent(dialogState: DialogState) @@ -77,7 +76,6 @@ interface OtherUserProfileBottomSheetEventsHandler { val PREVIEW = object : OtherUserProfileBottomSheetEventsHandler { override fun onChangeMemberRole(role: Conversation.Member.Role) {} override fun onMutingConversationStatusChange(conversationId: ConversationId?, status: MutedConversationStatus) {} - override fun onChangeFavoriteState(conversationId: ConversationId?, isFavorite: Boolean) {} override fun onMoveConversationToFolder(conversationId: ConversationId?) {} override fun onMoveConversationToArchive(dialogState: DialogState) {} override fun onClearConversationContent(dialogState: DialogState) {} diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreen.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreen.kt index b71d1d7701..98d5a77978 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreen.kt @@ -54,6 +54,7 @@ import androidx.hilt.navigation.compose.hiltViewModel import com.ramcosta.composedestinations.annotation.RootNavGraph import com.ramcosta.composedestinations.result.ResultBackNavigator import com.wire.android.R +import com.wire.android.di.hiltViewModelScoped import com.wire.android.navigation.BackStackMode import com.wire.android.navigation.NavigationCommand import com.wire.android.navigation.Navigator @@ -67,6 +68,9 @@ import com.wire.android.ui.common.VisibilityState import com.wire.android.ui.common.WireTabRow import com.wire.android.ui.common.bottomsheet.WireModalSheetLayout import com.wire.android.ui.common.bottomsheet.WireModalSheetState +import com.wire.android.ui.common.bottomsheet.folder.ChangeConversationFavoriteStateArgs +import com.wire.android.ui.common.bottomsheet.folder.ChangeConversationFavoriteVM +import com.wire.android.ui.common.bottomsheet.folder.ChangeConversationFavoriteVMImpl import com.wire.android.ui.common.bottomsheet.rememberWireModalSheetState import com.wire.android.ui.common.bottomsheet.show import com.wire.android.ui.common.button.WireButtonState @@ -104,6 +108,7 @@ import com.wire.android.ui.userprofile.group.RemoveConversationMemberState import com.wire.android.ui.userprofile.other.bottomsheet.OtherUserBottomSheetState import com.wire.android.ui.userprofile.other.bottomsheet.OtherUserProfileBottomSheetContent import com.wire.android.util.ui.PreviewMultipleThemes +import com.wire.android.util.ui.SnackBarMessageHandler import com.wire.android.util.ui.UIText import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.user.ConnectionState @@ -202,6 +207,7 @@ fun OtherUserProfileScreen( snackbarHostState.showSnackbar(it.asString(context.resources)) } } + LaunchedEffect(Unit) { viewModel.closeBottomSheet.collect { sheetState.hide() @@ -237,7 +243,11 @@ fun OtherProfileScreenContent( onOpenDeviceDetails: (Device) -> Unit = {}, onConversationMediaClick: () -> Unit = {}, navigateBack: () -> Unit = {}, - onLegalHoldLearnMoreClick: () -> Unit = {} + onLegalHoldLearnMoreClick: () -> Unit = {}, + changeConversationFavoriteViewModel: ChangeConversationFavoriteVM = + hiltViewModelScoped( + ChangeConversationFavoriteStateArgs + ) ) { val otherUserProfileScreenState = rememberOtherUserProfileScreenState() val blockUserDialogState = rememberVisibilityState() @@ -273,6 +283,8 @@ fun OtherProfileScreenContent( }) } + SnackBarMessageHandler(changeConversationFavoriteViewModel.infoMessage, onEmitted = closeBottomSheet) + val tabItems by remember(state) { derivedStateOf { listOfNotNull( @@ -358,6 +370,7 @@ fun OtherProfileScreenContent( unblockUser = unblockUserDialogState::show, clearContent = clearConversationDialogState::show, archivingStatusState = archivingConversationDialogState::show, + changeFavoriteState = changeConversationFavoriteViewModel::changeFavoriteState, closeBottomSheet = closeBottomSheet, ) } diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreenViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreenViewModel.kt index bfd89b9ca0..c57fa29f6d 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreenViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreenViewModel.kt @@ -69,8 +69,6 @@ import com.wire.kalium.logic.feature.conversation.UpdateConversationArchivedStat import com.wire.kalium.logic.feature.conversation.UpdateConversationMemberRoleResult import com.wire.kalium.logic.feature.conversation.UpdateConversationMemberRoleUseCase import com.wire.kalium.logic.feature.conversation.UpdateConversationMutedStatusUseCase -import com.wire.kalium.logic.feature.conversation.folder.AddConversationToFavoritesUseCase -import com.wire.kalium.logic.feature.conversation.folder.RemoveConversationFromFavoritesUseCase import com.wire.kalium.logic.feature.e2ei.usecase.GetUserE2eiCertificatesUseCase import com.wire.kalium.logic.feature.e2ei.usecase.IsOtherUserE2EIVerifiedUseCase import com.wire.kalium.logic.feature.user.GetUserInfoResult @@ -109,8 +107,6 @@ class OtherUserProfileScreenViewModel @Inject constructor( private val getUserE2eiCertificateStatus: IsOtherUserE2EIVerifiedUseCase, private val getUserE2eiCertificates: GetUserE2eiCertificatesUseCase, private val isOneToOneConversationCreated: IsOneToOneConversationCreatedUseCase, - private val addConversationToFavorites: AddConversationToFavoritesUseCase, - private val removeConversationFromFavorites: RemoveConversationFromFavoritesUseCase, savedStateHandle: SavedStateHandle ) : ViewModel(), OtherUserProfileEventsHandler, OtherUserProfileBottomSheetEventsHandler { @@ -311,19 +307,6 @@ class OtherUserProfileScreenViewModel @Inject constructor( } } - @Suppress("EmptyFunctionBlock") - override fun onChangeFavoriteState(conversationId: ConversationId?, isFavorite: Boolean) { - conversationId?.let { - viewModelScope.launch { - if (isFavorite) { - removeConversationFromFavorites(conversationId) - } else { - addConversationToFavorites(conversationId) - } - } - } - } - @Suppress("EmptyFunctionBlock") override fun onMoveConversationToFolder(conversationId: ConversationId?) { } diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/other/bottomsheet/OtherUserProfileBottomSheet.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/other/bottomsheet/OtherUserProfileBottomSheet.kt index 61cf880325..1757407daf 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/other/bottomsheet/OtherUserProfileBottomSheet.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/other/bottomsheet/OtherUserProfileBottomSheet.kt @@ -24,6 +24,7 @@ import com.wire.android.ui.common.bottomsheet.conversation.rememberConversationS import com.wire.android.ui.common.dialogs.BlockUserDialogState import com.wire.android.ui.common.dialogs.UnblockUserDialogState import com.wire.android.ui.home.conversationslist.model.DialogState +import com.wire.android.ui.home.conversationslist.model.GroupDialogState import com.wire.android.ui.userprofile.other.OtherUserProfileBottomSheetEventsHandler @Composable @@ -34,6 +35,7 @@ fun OtherUserProfileBottomSheetContent( archivingStatusState: (DialogState) -> Unit, blockUser: (BlockUserDialogState) -> Unit, unblockUser: (UnblockUserDialogState) -> Unit, + changeFavoriteState: (GroupDialogState, addToFavorite: Boolean) -> Unit, closeBottomSheet: () -> Unit, getBottomSheetVisibility: () -> Boolean ) { @@ -50,7 +52,7 @@ fun OtherUserProfileBottomSheetContent( mutedConversationStatus ) }, - changeFavoriteState = eventsHandler::onChangeFavoriteState, + changeFavoriteState = changeFavoriteState, moveConversationToFolder = eventsHandler::onMoveConversationToFolder, updateConversationArchiveStatus = { if (!it.isArchived) { diff --git a/app/src/main/kotlin/com/wire/android/util/ui/SnackBarMessageHandler.kt b/app/src/main/kotlin/com/wire/android/util/ui/SnackBarMessageHandler.kt index 525edb9aba..f523e0f254 100644 --- a/app/src/main/kotlin/com/wire/android/util/ui/SnackBarMessageHandler.kt +++ b/app/src/main/kotlin/com/wire/android/util/ui/SnackBarMessageHandler.kt @@ -29,13 +29,15 @@ import kotlinx.coroutines.flow.SharedFlow @Composable fun SnackBarMessageHandler( infoMessages: SharedFlow, - onActionClicked: (SnackBarMessage) -> Unit = {} + onEmitted: () -> Unit = {}, + onActionClicked: (SnackBarMessage) -> Unit = {}, ) { val context = LocalContext.current val snackbarHostState = LocalSnackbarHostState.current LaunchedEffect(Unit) { infoMessages.collect { + onEmitted() snackbarHostState.showSnackbar( message = it.uiText.asString(context.resources), actionLabel = it.actionLabel?.asString(context.resources), diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b82bb9aab2..83a43b717f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -906,6 +906,11 @@ Archive conversation? This conversation moves into your archive. You still get new messages, files, and calls, but no notifications. You can unarchive the conversation at any time. Archive + + “%s” was added to Favorites + “%s” was removed from Favorites + “%s” could not be added to Favorites + “%s” could not be removed from Favorites MessageComposeInputState transition HorizontalBouncingWritingPen transition diff --git a/app/src/test/kotlin/com/wire/android/framework/TestConversationItem.kt b/app/src/test/kotlin/com/wire/android/framework/TestConversationItem.kt index d5561428db..3ad1bcb795 100644 --- a/app/src/test/kotlin/com/wire/android/framework/TestConversationItem.kt +++ b/app/src/test/kotlin/com/wire/android/framework/TestConversationItem.kt @@ -44,7 +44,8 @@ object TestConversationItem { userId = UserId("value", "domain"), isArchived = false, mlsVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED, - proteusVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED + proteusVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED, + isFavorite = false ) val GROUP = ConversationItem.GroupConversation( @@ -59,7 +60,8 @@ object TestConversationItem { teamId = null, isArchived = false, mlsVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED, - proteusVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED + proteusVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED, + isFavorite = false ) val CONNECTION = ConversationItem.ConnectionConversation( diff --git a/app/src/test/kotlin/com/wire/android/ui/common/bottomsheet/conversation/ConversationSheetContentTest.kt b/app/src/test/kotlin/com/wire/android/ui/common/bottomsheet/conversation/ConversationSheetContentTest.kt index 09f6b2160a..9b9d8d5b27 100644 --- a/app/src/test/kotlin/com/wire/android/ui/common/bottomsheet/conversation/ConversationSheetContentTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/common/bottomsheet/conversation/ConversationSheetContentTest.kt @@ -41,7 +41,8 @@ class ConversationSheetContentTest { protocol = Conversation.ProtocolInfo.Proteus, mlsVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED, proteusVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED, - isUnderLegalHold = false + isUnderLegalHold = false, + isFavorite = false ) val givenParticipantsCount = 1 @@ -63,7 +64,8 @@ class ConversationSheetContentTest { protocol = Conversation.ProtocolInfo.Proteus, mlsVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED, proteusVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED, - isUnderLegalHold = false + isUnderLegalHold = false, + isFavorite = false ) val givenParticipantsCount = 3 @@ -85,7 +87,8 @@ class ConversationSheetContentTest { protocol = Conversation.ProtocolInfo.Proteus, mlsVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED, proteusVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED, - isUnderLegalHold = false + isUnderLegalHold = false, + isFavorite = false ) val givenParticipantsCount = 3 diff --git a/app/src/test/kotlin/com/wire/android/ui/common/bottomsheet/folder/ChangeConversationFavoriteVMTest.kt b/app/src/test/kotlin/com/wire/android/ui/common/bottomsheet/folder/ChangeConversationFavoriteVMTest.kt new file mode 100644 index 0000000000..6328fb096d --- /dev/null +++ b/app/src/test/kotlin/com/wire/android/ui/common/bottomsheet/folder/ChangeConversationFavoriteVMTest.kt @@ -0,0 +1,152 @@ +/* + * 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.bottomsheet.folder + +import app.cash.turbine.test +import com.wire.android.R +import com.wire.android.config.CoroutineTestExtension +import com.wire.android.framework.TestConversation +import com.wire.android.model.asSnackBarMessage +import com.wire.android.ui.home.conversationslist.model.GroupDialogState +import com.wire.android.util.ui.UIText +import com.wire.kalium.logic.CoreFailure +import com.wire.kalium.logic.feature.conversation.folder.AddConversationToFavoritesUseCase +import com.wire.kalium.logic.feature.conversation.folder.RemoveConversationFromFavoritesUseCase +import io.mockk.MockKAnnotations +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.impl.annotations.MockK +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith + +@ExtendWith(CoroutineTestExtension::class) +class ChangeConversationFavoriteVMTest { + + @Test + fun `given conversation is added to favorites successfully, then infoMessage should emit success`() = runTest { + val (arrangement, viewModel) = Arrangement().arrange { + withAddToFavoritesResult(AddConversationToFavoritesUseCase.Result.Success) + } + + viewModel.infoMessage.test { + + viewModel.changeFavoriteState(dialogState, addToFavorite = true) + + assertEquals( + UIText.StringResource(R.string.success_adding_to_favorite, conversationName).asSnackBarMessage(), + awaitItem() + ) + coVerify(exactly = 1) { + arrangement.addConversationToFavorites(dialogState.conversationId) + } + } + } + + @Test + fun `given conversation fails to add to favorites, then infoMessage should emit error`() = runTest { + val (arrangement, viewModel) = Arrangement().arrange { + withAddToFavoritesResult(AddConversationToFavoritesUseCase.Result.Failure(CoreFailure.Unknown(null))) + } + viewModel.infoMessage.test { + viewModel.changeFavoriteState(dialogState, addToFavorite = true) + + assertEquals( + UIText.StringResource(R.string.error_adding_to_favorite, conversationName).asSnackBarMessage(), + awaitItem() + ) + coVerify(exactly = 1) { + arrangement.addConversationToFavorites(dialogState.conversationId) + } + } + } + + @Test + fun `given conversation is removed from favorites successfully, then infoMessage should emit success`() = runTest { + val (arrangement, viewModel) = Arrangement().arrange { + withRemoveFromFavoritesResult(RemoveConversationFromFavoritesUseCase.Result.Success) + } + viewModel.infoMessage.test { + viewModel.changeFavoriteState(dialogState, addToFavorite = false) + + assertEquals( + UIText.StringResource(R.string.success_removing_from_favorite, conversationName).asSnackBarMessage(), + awaitItem() + ) + coVerify(exactly = 1) { + arrangement.removeConversationFromFavorites(dialogState.conversationId) + } + } + } + + @Test + fun `given conversation fails to remove from favorites, then infoMessage should emit error`() = runTest { + val (arrangement, viewModel) = Arrangement().arrange { + withRemoveFromFavoritesResult(RemoveConversationFromFavoritesUseCase.Result.Failure(CoreFailure.Unknown(null))) + } + viewModel.infoMessage.test { + viewModel.changeFavoriteState(dialogState, addToFavorite = false) + + assertEquals( + UIText.StringResource(R.string.error_removing_from_favorite, conversationName).asSnackBarMessage(), + awaitItem() + ) + coVerify(exactly = 1) { + arrangement.removeConversationFromFavorites(dialogState.conversationId) + } + } + } + + companion object { + val dialogState = GroupDialogState(conversationId = TestConversation.ID, conversationName = "Test Conversation") + val conversationName = dialogState.conversationName + } + + private class Arrangement { + + @MockK + lateinit var addConversationToFavorites: AddConversationToFavoritesUseCase + + @MockK + lateinit var removeConversationFromFavorites: RemoveConversationFromFavoritesUseCase + + private lateinit var viewModel: ChangeConversationFavoriteVM + + + init { + MockKAnnotations.init(this, relaxUnitFun = true) + } + + fun withAddToFavoritesResult(result: AddConversationToFavoritesUseCase.Result) = apply { + coEvery { addConversationToFavorites(any()) } returns result + } + + fun withRemoveFromFavoritesResult(result: RemoveConversationFromFavoritesUseCase.Result) = apply { + coEvery { removeConversationFromFavorites(any()) } returns result + } + + fun arrange(block: Arrangement.() -> Unit) = apply(block).let { + viewModel = ChangeConversationFavoriteVMImpl( + addConversationToFavorites, + removeConversationFromFavorites + ) + this to viewModel + } + } +} diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsViewModelTest.kt index 3ee92bb493..6c7b203bdb 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsViewModelTest.kt @@ -452,6 +452,7 @@ class GroupConversationDetailsViewModelTest { mlsVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED, proteusVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED, isUnderLegalHold = true, + isFavorite = false ) // When - Then assertEquals(expected, viewModel.conversationSheetContent) diff --git a/kalium b/kalium index 1cf021a57d..c8eef8a712 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 1cf021a57dd401626101de03e3c1f5a36de994f8 +Subproject commit c8eef8a712d78105cabfc5b4d7263fe8b1f18bc1