From 955306a4b07cbbb931db90c53ae70e2442977ba4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20=C5=BBerko?= Date: Mon, 25 Nov 2024 16:16:26 +0100 Subject: [PATCH 1/4] feat: add and remove conversation favorite [WPB-11639] (#3653) --- .../di/accountScoped/ConversationModule.kt | 10 ++ .../wire/android/mapper/ConversationMapper.kt | 2 + .../com/wire/android/model/SnackBarMessage.kt | 7 + .../conversation/ConversationSheetContent.kt | 12 +- .../conversation/ConversationSheetState.kt | 9 +- .../conversation/HomeSheetContent.kt | 53 ++++-- .../ChangeConversationFavoriteStateArgs.kt | 26 +++ .../folder/ChangeConversationFavoriteVM.kt | 71 ++++++++ .../details/GroupConversationDetailsScreen.kt | 15 +- .../GroupConversationDetailsViewModel.kt | 5 +- ...ersationDetailsBottomSheetEventsHandler.kt | 2 - .../ConversationListViewModel.kt | 6 - .../ConversationsScreenContent.kt | 13 +- .../common/ConversationItemFactory.kt | 18 ++- .../common/ConversationList.kt | 2 + .../model/ConversationItem.kt | 4 + .../other/OtherUserProfileEventsHandlers.kt | 2 - .../other/OtherUserProfileScreen.kt | 15 +- .../other/OtherUserProfileScreenViewModel.kt | 5 +- .../OtherUserProfileBottomSheet.kt | 4 +- .../android/util/ui/SnackBarMessageHandler.kt | 4 +- app/src/main/res/values/strings.xml | 6 + .../android/framework/TestConversationItem.kt | 6 +- .../ConversationSheetContentTest.kt | 9 +- .../ChangeConversationFavoriteVMTest.kt | 151 ++++++++++++++++++ .../GroupConversationDetailsViewModelTest.kt | 1 + kalium | 2 +- 27 files changed, 397 insertions(+), 63 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 baf45e7b76b..f726d098dc4 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 @@ -327,4 +327,14 @@ class ConversationModule { @Provides fun provideGetFavoriteFolderUseCase(conversationScope: ConversationScope) = conversationScope.getFavoriteFolder + + @ViewModelScoped + @Provides + fun provideAddConversationToFavoritesUseCase(conversationScope: ConversationScope) = + conversationScope.addConversationToFavorites + + @ViewModelScoped + @Provides + fun provideRemoveConversationFromFavoritesUseCase(conversationScope: ConversationScope) = + conversationScope.removeConversationFromFavorites } diff --git a/app/src/main/kotlin/com/wire/android/mapper/ConversationMapper.kt b/app/src/main/kotlin/com/wire/android/mapper/ConversationMapper.kt index 951ca60ceed..72af626fba8 100644 --- a/app/src/main/kotlin/com/wire/android/mapper/ConversationMapper.kt +++ b/app/src/main/kotlin/com/wire/android/mapper/ConversationMapper.kt @@ -65,6 +65,7 @@ fun ConversationDetailsWithEvents.toConversationItem( proteusVerificationStatus = conversationDetails.conversation.proteusVerificationStatus, hasNewActivitiesToShow = hasNewActivitiesToShow, searchQuery = searchQuery, + isFavorite = conversationDetails.isFavorite ) } @@ -101,6 +102,7 @@ fun ConversationDetailsWithEvents.toConversationItem( proteusVerificationStatus = conversationDetails.conversation.proteusVerificationStatus, hasNewActivitiesToShow = hasNewActivitiesToShow, searchQuery = searchQuery, + isFavorite = conversationDetails.isFavorite ) } 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 8a020d78792..a3fb5b97001 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 57007fae7aa..f7aeeec65a6 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, - addConversationToFavourites: () -> Unit, + changeFavoriteState: (GroupDialogState, addToFavorite: Boolean) -> Unit, moveConversationToFolder: () -> Unit, updateConversationArchiveStatus: (DialogState) -> Unit, clearConversationContent: (DialogState) -> Unit, @@ -54,9 +54,8 @@ fun ConversationSheetContent( ConversationOptionNavigation.Home -> { ConversationMainSheetContent( conversationSheetContent = conversationSheetState.conversationSheetContent!!, + changeFavoriteState = changeFavoriteState, // TODO(profile): enable when implemented -// -// addConversationToFavourites = addConversationToFavourites, // moveConversationToFolder = moveConversationToFolder, updateConversationArchiveStatus = updateConversationArchiveStatus, clearConversationContent = clearConversationContent, @@ -125,6 +124,7 @@ data class ConversationSheetContent( val mlsVerificationStatus: Conversation.VerificationStatus, val proteusVerificationStatus: Conversation.VerificationStatus, val isUnderLegalHold: Boolean, + val isFavorite: Boolean? ) { private val isSelfUserMember: Boolean get() = selfRole != null @@ -147,9 +147,9 @@ data class ConversationSheetContent( fun canUnblockUser(): Boolean = conversationTypeDetail is ConversationTypeDetail.Private && conversationTypeDetail.blockingState == BlockingState.BLOCKED - fun canAddToFavourite(): Boolean = - (conversationTypeDetail is ConversationTypeDetail.Private && conversationTypeDetail.blockingState != BlockingState.BLOCKED) - || conversationTypeDetail is ConversationTypeDetail.Group + fun canAddToFavourite(): Boolean = isFavorite != null && + ((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/ConversationSheetState.kt b/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/conversation/ConversationSheetState.kt index 0279e37367b..3455dd1315b 100644 --- a/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/conversation/ConversationSheetState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/conversation/ConversationSheetState.kt @@ -78,7 +78,8 @@ fun rememberConversationSheetState( protocol = Conversation.ProtocolInfo.Proteus, mlsVerificationStatus = Conversation.VerificationStatus.VERIFIED, proteusVerificationStatus = Conversation.VerificationStatus.VERIFIED, - isUnderLegalHold = showLegalHoldIndicator + isUnderLegalHold = showLegalHoldIndicator, + isFavorite = isFavorite ) } } @@ -102,7 +103,8 @@ fun rememberConversationSheetState( protocol = Conversation.ProtocolInfo.Proteus, mlsVerificationStatus = Conversation.VerificationStatus.VERIFIED, proteusVerificationStatus = Conversation.VerificationStatus.VERIFIED, - isUnderLegalHold = showLegalHoldIndicator + isUnderLegalHold = showLegalHoldIndicator, + isFavorite = isFavorite ) } } @@ -122,7 +124,8 @@ fun rememberConversationSheetState( protocol = Conversation.ProtocolInfo.Proteus, mlsVerificationStatus = Conversation.VerificationStatus.VERIFIED, proteusVerificationStatus = Conversation.VerificationStatus.VERIFIED, - isUnderLegalHold = showLegalHoldIndicator + isUnderLegalHold = showLegalHoldIndicator, + isFavorite = null ) } } 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 ab514914c85..c06603826aa 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 @@ -53,12 +53,14 @@ import com.wire.android.ui.theme.wireTypography import com.wire.kalium.logic.data.conversation.MutedConversationStatus 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 -// addConversationToFavourites: () -> Unit, -// moveConversationToFolder: () -> Unit, + changeFavoriteState: (dialogState: GroupDialogState, addToFavorite: Boolean) -> Unit, + // TODO(profile): enable when implemented + // moveConversationToFolder: () -> Unit, updateConversationArchiveStatus: (DialogState) -> Unit, clearConversationContent: (DialogState) -> Unit, blockUserClick: (BlockUserDialogState) -> Unit, @@ -108,21 +110,38 @@ internal fun ConversationMainSheetContent( ) } } + + if (conversationSheetContent.canAddToFavourite() && !conversationSheetContent.isArchived) { + conversationSheetContent.isFavorite?.let { isFavorite -> + add { + MenuBottomSheetItem( + title = stringResource( + if (isFavorite) { + R.string.label_remove_from_favourites + } else { + R.string.label_add_to_favourites + } + ), + leading = { + MenuItemIcon( + id = R.drawable.ic_favourite, + contentDescription = null + ) + }, + onItemClick = { + changeFavoriteState( + GroupDialogState( + conversationSheetContent.conversationId, + conversationSheetContent.title + ), + !isFavorite + ) + } + ) + } + } + } // TODO(profile): enable when implemented -// -// if (conversationSheetContent.canAddToFavourite()) -// add { -// MenuBottomSheetItem( -// title = stringResource(R.string.label_add_to_favourites), -// icon = { -// MenuItemIcon( -// id = R.drawable.ic_favourite, -// contentDescription = stringResource(R.string.content_description_add_to_favourite), -// ) -// }, -// onItemClick = addConversationToFavourites -// ) -// } // add { // MenuBottomSheetItem( // icon = { 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 00000000000..a3dd9b6b5b7 --- /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 00000000000..6d2c0a14cd2 --- /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 a6d19e62bc7..b067f167aeb 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( ) } }, - addConversationToFavourites = bottomSheetEventsHandler::onAddConversationToFavourites, + changeFavoriteState = changeConversationFavoriteStateViewModel::changeFavoriteState, moveConversationToFolder = bottomSheetEventsHandler::onMoveConversationToFolder, updateConversationArchiveStatus = { // Only show the confirmation dialog if the conversation is not archived @@ -597,7 +605,8 @@ fun PreviewGroupConversationDetails() { ), mlsVerificationStatus = Conversation.VerificationStatus.VERIFIED, isUnderLegalHold = false, - proteusVerificationStatus = Conversation.VerificationStatus.VERIFIED + proteusVerificationStatus = Conversation.VerificationStatus.VERIFIED, + isFavorite = false ), bottomSheetEventsHandler = GroupConversationDetailsBottomSheetEventsHandler.PREVIEW, onBackPressed = {}, 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 31063e6a052..e5704c32097 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 @@ -158,6 +158,7 @@ class GroupConversationDetailsViewModel @Inject constructor( mlsVerificationStatus = groupDetails.conversation.mlsVerificationStatus, proteusVerificationStatus = groupDetails.conversation.proteusVerificationStatus, isUnderLegalHold = groupDetails.conversation.legalHoldStatus.showLegalHoldIndicator(), + isFavorite = groupDetails.isFavorite ) updateState( @@ -374,10 +375,6 @@ class GroupConversationDetailsViewModel @Inject constructor( } } - @Suppress("EmptyFunctionBlock") - override fun onAddConversationToFavourites(conversationId: 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 16a000ea627..6a0d8132b13 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 onAddConversationToFavourites(conversationId: ConversationId? = null) fun onMoveConversationToFolder(conversationId: ConversationId? = null) fun updateConversationArchiveStatus( dialogState: DialogState, @@ -47,7 +46,6 @@ interface GroupConversationDetailsBottomSheetEventsHandler { ) { } - override fun onAddConversationToFavourites(conversationId: ConversationId?) {} 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 2b5c4b66748..a4a225b4c59 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 @@ -110,7 +110,6 @@ interface ConversationListViewModel { fun leaveGroup(leaveGroupState: GroupDialogState) {} fun clearConversationContent(dialogState: DialogState) {} fun muteConversation(conversationId: ConversationId?, mutedConversationStatus: MutedConversationStatus) {} - fun addConversationToFavourites() {} fun moveConversationToFolder() {} fun searchQueryChanged(searchQuery: String) {} } @@ -367,11 +366,6 @@ class ConversationListViewModelImpl @AssistedInject constructor( } } - // TODO: needs to be implemented - @Suppress("EmptyFunctionBlock") - override fun addConversationToFavourites() { - } - // 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 4d29db15cb6..244fc837067 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 ) }, - addConversationToFavourites = conversationListViewModel::addConversationToFavourites, + 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/home/conversationslist/common/ConversationItemFactory.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/common/ConversationItemFactory.kt index 9a8f216b91b..180824ee0a9 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/common/ConversationItemFactory.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/common/ConversationItemFactory.kt @@ -311,7 +311,8 @@ fun PreviewGroupConversationItemWithUnreadCount() = WireTheme { teamId = null, isArchived = false, mlsVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED, - proteusVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED + proteusVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED, + isFavorite = false ), modifier = Modifier, isSelectableItem = false, @@ -336,7 +337,8 @@ fun PreviewGroupConversationItemWithNoBadges() = WireTheme { teamId = null, isArchived = false, mlsVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED, - proteusVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED + proteusVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED, + isFavorite = false ), modifier = Modifier, isSelectableItem = false, @@ -363,7 +365,8 @@ fun PreviewGroupConversationItemWithLastDeletedMessage() = WireTheme { teamId = null, isArchived = false, mlsVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED, - proteusVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED + proteusVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED, + isFavorite = false ), modifier = Modifier, isSelectableItem = false, @@ -388,7 +391,8 @@ fun PreviewGroupConversationItemWithMutedBadgeAndUnreadMentionBadge() = WireThem teamId = null, isArchived = false, mlsVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED, - proteusVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED + proteusVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED, + isFavorite = false ), modifier = Modifier, isSelectableItem = false, @@ -414,7 +418,8 @@ fun PreviewGroupConversationItemWithOngoingCall() = WireTheme { hasOnGoingCall = true, isArchived = false, mlsVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED, - proteusVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED + proteusVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED, + isFavorite = false ), modifier = Modifier, isSelectableItem = false, @@ -496,7 +501,8 @@ fun PreviewPrivateConversationItemWithBlockedBadge() = WireTheme { userId = UserId("value", "domain"), isArchived = false, mlsVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED, - proteusVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED + proteusVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED, + isFavorite = false ), modifier = Modifier, isSelectableItem = false, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/common/ConversationList.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/common/ConversationList.kt index e57d257d11d..2fe1ea862ce 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/common/ConversationList.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/common/ConversationList.kt @@ -204,6 +204,7 @@ fun previewConversationList(count: Int, startIndex: Int = 0, unread: Boolean = f mlsVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED, proteusVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED, searchQuery = searchQuery, + isFavorite = false ) ) @@ -222,6 +223,7 @@ fun previewConversationList(count: Int, startIndex: Int = 0, unread: Boolean = f mlsVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED, proteusVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED, searchQuery = searchQuery, + isFavorite = false ) ) } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/model/ConversationItem.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/model/ConversationItem.kt index dbc5f36305c..46726d55209 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/model/ConversationItem.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/model/ConversationItem.kt @@ -38,6 +38,7 @@ sealed class ConversationItem : ConversationFolderItem { abstract val badgeEventType: BadgeEventType abstract val teamId: TeamId? abstract val isArchived: Boolean + abstract val isFavorite: Boolean abstract val mlsVerificationStatus: Conversation.VerificationStatus abstract val proteusVerificationStatus: Conversation.VerificationStatus abstract val hasNewActivitiesToShow: Boolean @@ -58,6 +59,7 @@ sealed class ConversationItem : ConversationFolderItem { override val badgeEventType: BadgeEventType, override val teamId: TeamId?, override val isArchived: Boolean, + override val isFavorite: Boolean, override val mlsVerificationStatus: Conversation.VerificationStatus, override val proteusVerificationStatus: Conversation.VerificationStatus, override val hasNewActivitiesToShow: Boolean = false, @@ -76,6 +78,7 @@ sealed class ConversationItem : ConversationFolderItem { override val badgeEventType: BadgeEventType, override val teamId: TeamId?, override val isArchived: Boolean, + override val isFavorite: Boolean, override val mlsVerificationStatus: Conversation.VerificationStatus, override val proteusVerificationStatus: Conversation.VerificationStatus, override val hasNewActivitiesToShow: Boolean = false, @@ -91,6 +94,7 @@ sealed class ConversationItem : ConversationFolderItem { override val lastMessageContent: UILastMessageContent?, override val badgeEventType: BadgeEventType, override val isArchived: Boolean = false, + override val isFavorite: Boolean = false, override val hasNewActivitiesToShow: Boolean = false, override val searchQuery: String = "", ) : ConversationItem() { 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 42bad51159b..162b14cee3c 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 onAddConversationToFavourites(conversationId: ConversationId? = null) 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 onAddConversationToFavourites(conversationId: ConversationId?) {} 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 b71d1d77019..98d5a77978d 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 f11a08c7030..c57fa29f6d7 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 @@ -307,10 +307,6 @@ class OtherUserProfileScreenViewModel @Inject constructor( } } - @Suppress("EmptyFunctionBlock") - override fun onAddConversationToFavourites(conversationId: ConversationId?) { - } - @Suppress("EmptyFunctionBlock") override fun onMoveConversationToFolder(conversationId: ConversationId?) { } @@ -421,6 +417,7 @@ class OtherUserProfileScreenViewModel @Inject constructor( mlsVerificationStatus = conversation.mlsVerificationStatus, proteusVerificationStatus = conversation.proteusVerificationStatus, isUnderLegalHold = conversation.legalHoldStatus.showLegalHoldIndicator(), + isFavorite = null ) } ) 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 ae26a0e52a5..1757407daf3 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 ) }, - addConversationToFavourites = eventsHandler::onAddConversationToFavourites, + 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 525edb9aba8..f523e0f2540 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 506a4495b54..5b8ddbbb05c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -636,6 +636,7 @@ Notifications Add to Favorites + Remove from Favorites Move to Folder Move to Archive Unarchive @@ -904,6 +905,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 d5561428db9..3ad1bcb7955 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 09f6b2160ab..9b9d8d5b273 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 00000000000..330ef97d5c9 --- /dev/null +++ b/app/src/test/kotlin/com/wire/android/ui/common/bottomsheet/folder/ChangeConversationFavoriteVMTest.kt @@ -0,0 +1,151 @@ +/* + * 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 3ee92bb493e..6c7b203bdb3 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 08c0cffe74b..c7b96a2584d 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 08c0cffe74b9523c7464e3645eb79f2ca7d59d3f +Subproject commit c7b96a2584d6cba1619501b917a90b757785b7ac From 8d01a7dd496f4af2ae3dd261fec01fc08c2a9ffe Mon Sep 17 00:00:00 2001 From: Oussama Hassine Date: Tue, 26 Nov 2024 10:31:22 +0100 Subject: [PATCH 2/4] feat: adjust accessibility strings for personal to team migration feature (WPB-14347) (#3651) --- .../com/wire/android/ui/home/HomeTopBar.kt | 7 ++++++- .../ui/userprofile/self/CreateTeamInfoCard.kt | 7 ++++--- .../teammigration/TeamMigrationScreen.kt | 16 +++++++++++++++- .../teammigration/TeamMigrationState.kt | 3 ++- .../teammigration/TeamMigrationViewModel.kt | 4 ++++ .../teammigration/common/BottomLineButtons.kt | 7 ++++++- .../step1/TeamMigrationTeamPlanStepScreen.kt | 7 +++++-- .../step2/TeamMigrationTeamNameStepScreen.kt | 8 ++++++-- .../step3/TeamMigrationConfirmationStepScreen.kt | 6 +++++- .../step4/TeamMigrationDoneStepScreen.kt | 9 ++++++++- app/src/main/res/values/strings.xml | 7 +++++++ .../com/wire/android/ui/theme/WireDimensions.kt | 2 -- 12 files changed, 68 insertions(+), 15 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/HomeTopBar.kt b/app/src/main/kotlin/com/wire/android/ui/home/HomeTopBar.kt index f2ab8d539ad..f5a247fced9 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/HomeTopBar.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/HomeTopBar.kt @@ -69,6 +69,11 @@ fun HomeTopBar( ) } val openLabel = stringResource(R.string.content_description_open_label) + val contentDescription = if (shouldShowCreateTeamUnreadIndicator) { + stringResource(R.string.content_description_home_profile_btn_with_notification) + } else { + stringResource(R.string.content_description_home_profile_btn) + } UserProfileAvatar( avatarData = userAvatarData, clickable = remember { @@ -76,7 +81,7 @@ fun HomeTopBar( }, type = UserProfileAvatarType.WithIndicators.RegularUser(legalHoldIndicatorVisible = withLegalHoldIndicator), shouldShowCreateTeamUnreadIndicator = shouldShowCreateTeamUnreadIndicator, - contentDescription = stringResource(R.string.content_description_home_profile_btn) + contentDescription = contentDescription ) }, elevation = elevation, diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/CreateTeamInfoCard.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/CreateTeamInfoCard.kt index f37355bfa39..3741472dd9d 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/CreateTeamInfoCard.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/CreateTeamInfoCard.kt @@ -22,7 +22,6 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width import androidx.compose.material.Icon import androidx.compose.material.Text import androidx.compose.material3.CardDefaults @@ -36,6 +35,7 @@ import com.wire.android.R import com.wire.android.ui.common.button.WireSecondaryButton import com.wire.android.ui.common.colorsScheme import com.wire.android.ui.common.dimensions +import com.wire.android.ui.theme.WireTheme import com.wire.android.ui.theme.wireTypography import com.wire.android.util.ui.PreviewMultipleThemes @@ -83,7 +83,6 @@ fun CreateTeamInfoCard( WireSecondaryButton( modifier = Modifier .padding(dimensions().spacing8x) - .width(dimensions().createTeamInfoCardButtonWidth) .height(dimensions().createTeamInfoCardButtonHeight), text = stringResource(R.string.user_profile_create_team_card_button), onClick = onCreateAccount, @@ -97,5 +96,7 @@ fun CreateTeamInfoCard( @PreviewMultipleThemes @Composable fun PreviewCreateTeamInfoCard() { - CreateTeamInfoCard({ }) + WireTheme { + CreateTeamInfoCard({ }) + } } diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/TeamMigrationScreen.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/TeamMigrationScreen.kt index 42d921e53d3..c7ed3f3d9c5 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/TeamMigrationScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/TeamMigrationScreen.kt @@ -22,6 +22,7 @@ import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon @@ -54,6 +55,10 @@ import com.wire.android.ui.common.dimensions import com.wire.android.ui.common.preview.MultipleThemePreviews import com.wire.android.ui.theme.WireTheme import com.wire.android.ui.userprofile.teammigration.common.ConfirmMigrationLeaveDialog +import com.wire.android.ui.userprofile.teammigration.step1.TEAM_MIGRATION_TEAM_PLAN_STEP +import com.wire.android.ui.userprofile.teammigration.step2.TEAM_MIGRATION_TEAM_NAME_STEP +import com.wire.android.ui.userprofile.teammigration.step3.TEAM_MIGRATION_CONFIRMATION_STEP +import com.wire.android.ui.userprofile.teammigration.step4.TEAM_MIGRATION_DONE_STEP @OptIn(ExperimentalMaterialNavigationApi::class, ExperimentalAnimationApi::class) @WireDestination(style = PopUpNavigationAnimation::class) @@ -82,6 +87,7 @@ fun TeamMigrationScreen( Column( modifier = modifier .padding(top = dimensions().spacing32x) + .navigationBarsPadding() .clip( shape = RoundedCornerShape( dimensions().corner16x, @@ -91,6 +97,14 @@ fun TeamMigrationScreen( .fillMaxSize() .background(color = colorsScheme().surface) ) { + val closeIconContentDescription = when (teamMigrationViewModel.teamMigrationState.currentStep) { + TEAM_MIGRATION_TEAM_PLAN_STEP -> stringResource(R.string.personal_to_team_migration_close_team_account_content_description) + TEAM_MIGRATION_TEAM_NAME_STEP -> stringResource(R.string.personal_to_team_migration_close_team_name_content_description) + TEAM_MIGRATION_CONFIRMATION_STEP -> stringResource(R.string.personal_to_team_migration_close_confirmation_content_description) + TEAM_MIGRATION_DONE_STEP -> stringResource(R.string.personal_to_team_migration_close_team_created_content_description) + else -> stringResource(R.string.personal_to_team_migration_close_icon_content_description) + } + IconButton( modifier = Modifier.align(alignment = Alignment.End), onClick = { @@ -105,7 +119,7 @@ fun TeamMigrationScreen( ) { Icon( painter = painterResource(id = R.drawable.ic_close), - contentDescription = stringResource(R.string.personal_to_team_migration_close_icon_content_description) + contentDescription = closeIconContentDescription ) } diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/TeamMigrationState.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/TeamMigrationState.kt index 0399b9c0797..3a045cfd045 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/TeamMigrationState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/TeamMigrationState.kt @@ -23,5 +23,6 @@ import com.wire.kalium.logic.CoreFailure data class TeamMigrationState( val teamNameTextState: TextFieldState = TextFieldState(), val shouldShowMigrationLeaveDialog: Boolean = false, - val migrationFailure: CoreFailure? = null, + val currentStep: Int = 0, + val migrationFailure: CoreFailure? = null ) 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 9fd2d31daba..b8652aa171b 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 @@ -63,6 +63,10 @@ class TeamMigrationViewModel @Inject constructor( ) } + fun setCurrentStep(step: Int) { + teamMigrationState = teamMigrationState.copy(currentStep = step) + } + fun sendPersonalTeamCreationFlowCanceledEvent( modalLeaveClicked: Boolean? = null, modalContinueClicked: Boolean? = null diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/common/BottomLineButtons.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/common/BottomLineButtons.kt index 8b2fe3a4db9..65ef95d4bac 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/common/BottomLineButtons.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/common/BottomLineButtons.kt @@ -27,6 +27,8 @@ import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics import com.wire.android.R import com.wire.android.ui.common.button.WireButtonState import com.wire.android.ui.common.button.WirePrimaryButton @@ -41,6 +43,7 @@ fun BottomLineButtons( isContinueButtonEnabled: Boolean, modifier: Modifier = Modifier, isBackButtonVisible: Boolean = true, + backButtonContentDescription: String = stringResource(R.string.personal_to_team_migration_back_button_label), onBack: () -> Unit = { }, onContinue: () -> Unit = { } ) { @@ -60,7 +63,9 @@ fun BottomLineButtons( ) { if (isBackButtonVisible) { WireSecondaryButton( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .fillMaxWidth() + .semantics(true) { contentDescription = backButtonContentDescription }, text = stringResource(R.string.personal_to_team_migration_back_button_label), onClick = onBack ) diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/step1/TeamMigrationTeamPlanStepScreen.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/step1/TeamMigrationTeamPlanStepScreen.kt index f73d88d4029..7f34cda3a13 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/step1/TeamMigrationTeamPlanStepScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/step1/TeamMigrationTeamPlanStepScreen.kt @@ -54,12 +54,14 @@ import com.wire.android.ui.common.dimensions import com.wire.android.ui.destinations.TeamMigrationTeamNameStepScreenDestination import com.wire.android.ui.theme.WireTheme import com.wire.android.ui.theme.wireTypography -import com.wire.android.ui.userprofile.teammigration.common.BottomLineButtons import com.wire.android.ui.userprofile.teammigration.PersonalToTeamMigrationNavGraph import com.wire.android.ui.userprofile.teammigration.TeamMigrationViewModel +import com.wire.android.ui.userprofile.teammigration.common.BottomLineButtons import com.wire.android.util.CustomTabsHelper import com.wire.android.util.ui.PreviewMultipleThemes +const val TEAM_MIGRATION_TEAM_PLAN_STEP = 1 + @PersonalToTeamMigrationNavGraph(start = true) @WireDestination( style = SlideNavigationAnimation::class @@ -76,7 +78,8 @@ fun TeamMigrationTeamPlanStepScreen( ) LaunchedEffect(Unit) { - teamMigrationViewModel.sendPersonalTeamCreationFlowStartedEvent(1) + teamMigrationViewModel.sendPersonalTeamCreationFlowStartedEvent(TEAM_MIGRATION_TEAM_PLAN_STEP) + teamMigrationViewModel.setCurrentStep(TEAM_MIGRATION_TEAM_PLAN_STEP) } } diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/step2/TeamMigrationTeamNameStepScreen.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/step2/TeamMigrationTeamNameStepScreen.kt index 468c20d4f71..aa1e8a9ecf6 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/step2/TeamMigrationTeamNameStepScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/step2/TeamMigrationTeamNameStepScreen.kt @@ -44,11 +44,13 @@ import com.wire.android.ui.common.textfield.WireTextField import com.wire.android.ui.destinations.TeamMigrationConfirmationStepScreenDestination import com.wire.android.ui.theme.WireTheme import com.wire.android.ui.theme.wireTypography -import com.wire.android.ui.userprofile.teammigration.common.BottomLineButtons import com.wire.android.ui.userprofile.teammigration.PersonalToTeamMigrationNavGraph import com.wire.android.ui.userprofile.teammigration.TeamMigrationViewModel +import com.wire.android.ui.userprofile.teammigration.common.BottomLineButtons import com.wire.android.util.ui.PreviewMultipleThemes +const val TEAM_MIGRATION_TEAM_NAME_STEP = 2 + @PersonalToTeamMigrationNavGraph @WireDestination( style = SlideNavigationAnimation::class @@ -68,7 +70,8 @@ fun TeamMigrationTeamNameStepScreen( teamNameTextFieldState = teamMigrationViewModel.teamMigrationState.teamNameTextState ) LaunchedEffect(Unit) { - teamMigrationViewModel.sendPersonalTeamCreationFlowStartedEvent(2) + teamMigrationViewModel.sendPersonalTeamCreationFlowStartedEvent(TEAM_MIGRATION_TEAM_NAME_STEP) + teamMigrationViewModel.setCurrentStep(TEAM_MIGRATION_TEAM_NAME_STEP) } } @@ -130,6 +133,7 @@ private fun TeamMigrationTeamNameStepScreenContent( BottomLineButtons( isContinueButtonEnabled = isContinueButtonEnabled, onContinue = onContinueButtonClicked, + backButtonContentDescription = stringResource(R.string.personal_to_team_migration_back_button_team_name_content_description), onBack = onBackButtonClicked ) } diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/step3/TeamMigrationConfirmationStepScreen.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/step3/TeamMigrationConfirmationStepScreen.kt index 9be96dcb197..f3254d79370 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/step3/TeamMigrationConfirmationStepScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/step3/TeamMigrationConfirmationStepScreen.kt @@ -59,6 +59,8 @@ import com.wire.android.ui.userprofile.teammigration.common.BulletList import com.wire.android.util.CustomTabsHelper import com.wire.android.util.ui.PreviewMultipleThemes +const val TEAM_MIGRATION_CONFIRMATION_STEP = 3 + @PersonalToTeamMigrationNavGraph @WireDestination( style = SlideNavigationAnimation::class @@ -86,7 +88,8 @@ fun TeamMigrationConfirmationStepScreen( HandleErrors(state, teamMigrationViewModel::failureHandled) LaunchedEffect(Unit) { - teamMigrationViewModel.sendPersonalTeamCreationFlowStartedEvent(3) + teamMigrationViewModel.sendPersonalTeamCreationFlowStartedEvent(TEAM_MIGRATION_CONFIRMATION_STEP) + teamMigrationViewModel.setCurrentStep(TEAM_MIGRATION_CONFIRMATION_STEP) } } @@ -185,6 +188,7 @@ private fun TeamMigrationConfirmationStepScreenContent( BottomLineButtons( isContinueButtonEnabled = isContinueButtonEnabled, onContinue = onContinueButtonClicked, + backButtonContentDescription = stringResource(R.string.personal_to_team_migration_back_button_confirmation_content_description), onBack = onBackPressed ) } diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/step4/TeamMigrationDoneStepScreen.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/step4/TeamMigrationDoneStepScreen.kt index cd2b4c3f1b9..2f582d61aa3 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/step4/TeamMigrationDoneStepScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/step4/TeamMigrationDoneStepScreen.kt @@ -27,6 +27,7 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @@ -45,12 +46,14 @@ import com.wire.android.ui.common.spacers.VerticalSpace.x32 import com.wire.android.ui.destinations.HomeScreenDestination import com.wire.android.ui.theme.WireTheme import com.wire.android.ui.theme.wireTypography -import com.wire.android.ui.userprofile.teammigration.common.BulletList import com.wire.android.ui.userprofile.teammigration.PersonalToTeamMigrationNavGraph import com.wire.android.ui.userprofile.teammigration.TeamMigrationViewModel +import com.wire.android.ui.userprofile.teammigration.common.BulletList import com.wire.android.util.CustomTabsHelper import com.wire.android.util.ui.PreviewMultipleThemes +const val TEAM_MIGRATION_DONE_STEP = 4 + @PersonalToTeamMigrationNavGraph @WireDestination( style = SlideNavigationAnimation::class @@ -84,6 +87,10 @@ fun TeamMigrationDoneStepScreen( teamName = teamMigrationViewModel.teamMigrationState.teamNameTextState.text.toString() ) + LaunchedEffect(Unit) { + teamMigrationViewModel.setCurrentStep(TEAM_MIGRATION_DONE_STEP) + } + BackHandler { } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5b8ddbbb05c..826347c0d64 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -198,6 +198,7 @@ Search people by name or username Go back to add participants view Your profile + Your profile, one unread hint. open join a call share @@ -1581,7 +1582,13 @@ In group conversations, the group admin can overwrite this setting. Step %1$d of 4 Back to Wire Back + Go back to team account overview + Go back to team name view Close team migration flow + Close team account overview + Close team name view + Close confirmation view + Close team created view Team Account Transform your personal account into a team account to get more out of your collaboration. diff --git a/core/ui-common/src/main/kotlin/com/wire/android/ui/theme/WireDimensions.kt b/core/ui-common/src/main/kotlin/com/wire/android/ui/theme/WireDimensions.kt index 5a108fc6a53..324e9477336 100644 --- a/core/ui-common/src/main/kotlin/com/wire/android/ui/theme/WireDimensions.kt +++ b/core/ui-common/src/main/kotlin/com/wire/android/ui/theme/WireDimensions.kt @@ -115,7 +115,6 @@ data class WireDimensions( val dialogCardMargin: Dp, // UserProfile val userProfileOtherAccItemHeight: Dp, - val createTeamInfoCardButtonWidth: Dp, val createTeamInfoCardButtonHeight: Dp, // Profile Image val imagePreviewHeight: Dp, @@ -222,7 +221,6 @@ private val DefaultPhonePortraitWireDimensions: WireDimensions = WireDimensions( avatarStatusSize = 14.dp, unReadIndicatorSize = 16.dp, avatarStatusBorderWidth = 2.dp, - createTeamInfoCardButtonWidth = 120.dp, createTeamInfoCardButtonHeight = 32.dp, avatarTemporaryUserBorderWidth = 2.dp, avatarBigTemporaryUserBorderWidth = 4.dp, From 286ad3f627f4acce7e63c0071407d80f0c6f1a88 Mon Sep 17 00:00:00 2001 From: boris Date: Tue, 26 Nov 2024 12:30:39 +0200 Subject: [PATCH 3/4] fix: Listen few audios at the same time [WPB-11180] (#3639) --- .../ConversationAudioMessagePlayer.kt | 64 +++++++++++++++---- .../messages/ConversationMessagesViewModel.kt | 7 +- .../ConversationAudioMessagePlayerTest.kt | 34 ++++++++-- ...onversationMessagesViewModelArrangement.kt | 8 ++- 4 files changed, 92 insertions(+), 21 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/media/audiomessage/ConversationAudioMessagePlayer.kt b/app/src/main/kotlin/com/wire/android/media/audiomessage/ConversationAudioMessagePlayer.kt index 8fb90a99f6b..3f6c08ed1be 100644 --- a/app/src/main/kotlin/com/wire/android/media/audiomessage/ConversationAudioMessagePlayer.kt +++ b/app/src/main/kotlin/com/wire/android/media/audiomessage/ConversationAudioMessagePlayer.kt @@ -21,9 +21,11 @@ import android.content.Context import android.media.MediaPlayer import android.media.MediaPlayer.SEEK_CLOSEST_SYNC import android.net.Uri +import com.wire.android.di.KaliumCoreLogic +import com.wire.kalium.logic.CoreLogic import com.wire.kalium.logic.data.id.ConversationId -import com.wire.kalium.logic.feature.asset.GetMessageAssetUseCase import com.wire.kalium.logic.feature.asset.MessageAssetResult +import com.wire.kalium.logic.feature.session.CurrentSessionResult import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay @@ -34,14 +36,44 @@ import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.launch import javax.inject.Inject +import javax.inject.Singleton -class ConversationAudioMessagePlayer +@Singleton +class ConversationAudioMessagePlayerProvider @Inject constructor( private val context: Context, private val audioMediaPlayer: MediaPlayer, - private val getMessageAsset: GetMessageAssetUseCase + @KaliumCoreLogic private val coreLogic: CoreLogic, +) { + private var player: ConversationAudioMessagePlayer? = null + private var usageCount: Int = 0 + + @Synchronized + fun provide(): ConversationAudioMessagePlayer { + val player = player ?: ConversationAudioMessagePlayer(context, audioMediaPlayer, coreLogic).also { player = it } + usageCount++ + + return player + } + + @Synchronized + fun onCleared() { + usageCount-- + if (usageCount <= 0) { + player?.close() + player = null + } + } +} + +class ConversationAudioMessagePlayer +internal constructor( + private val context: Context, + private val audioMediaPlayer: MediaPlayer, + @KaliumCoreLogic private val coreLogic: CoreLogic, ) { private companion object { const val UPDATE_POSITION_INTERVAL_IN_MS = 1000L @@ -137,7 +169,7 @@ class ConversationAudioMessagePlayer } audioMessageStateHistory - } + }.onStart { emit(audioMessageStateHistory) } private var currentAudioMessageId: String? = null @@ -169,10 +201,10 @@ class ConversationAudioMessagePlayer } private suspend fun stopCurrentlyPlayingAudioMessage() { - if (currentAudioMessageId != null) { - val currentAudioState = audioMessageStateHistory[currentAudioMessageId] + currentAudioMessageId?.let { + val currentAudioState = audioMessageStateHistory[it] if (currentAudioState?.audioMediaPlayingState != AudioMediaPlayingState.Fetching) { - stop(currentAudioMessageId!!) + stop(it) } } } @@ -194,6 +226,9 @@ class ConversationAudioMessagePlayer coroutineScope { launch { + val currentAccountResult = coreLogic.getGlobalScope().session.currentSession() + if (currentAccountResult is CurrentSessionResult.Failure) return@launch + audioMessageStateUpdate.emit( AudioMediaPlayerStateUpdate.AudioMediaPlayingStateUpdate( messageId, @@ -201,7 +236,12 @@ class ConversationAudioMessagePlayer ) ) - when (val result = getMessageAsset(conversationId, messageId).await()) { + val assetMessage = coreLogic + .getSessionScope((currentAccountResult as CurrentSessionResult.Success).accountInfo.userId) + .messages + .getAssetMessage(conversationId, messageId) + + when (val result = assetMessage.await()) { is MessageAssetResult.Success -> { audioMessageStateUpdate.emit( AudioMediaPlayerStateUpdate.AudioMediaPlayingStateUpdate( @@ -219,9 +259,7 @@ class ConversationAudioMessagePlayer ) audioMediaPlayer.prepare() - if (position != null) { - audioMediaPlayer.seekTo(position) - } + if (position != null) audioMediaPlayer.seekTo(position) audioMediaPlayer.start() @@ -292,7 +330,7 @@ class ConversationAudioMessagePlayer ) } - fun close() { - audioMediaPlayer.release() + internal fun close() { + audioMediaPlayer.reset() } } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/ConversationMessagesViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/ConversationMessagesViewModel.kt index d7ffb2d2165..84aef9ce71f 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/ConversationMessagesViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/ConversationMessagesViewModel.kt @@ -26,7 +26,7 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope import com.wire.android.R import com.wire.android.appLogger -import com.wire.android.media.audiomessage.ConversationAudioMessagePlayer +import com.wire.android.media.audiomessage.ConversationAudioMessagePlayerProvider import com.wire.android.model.SnackBarMessage import com.wire.android.navigation.SavedStateViewModel import com.wire.android.ui.home.conversations.ConversationNavArgs @@ -98,7 +98,7 @@ class ConversationMessagesViewModel @Inject constructor( private val getMessageForConversation: GetMessagesForConversationUseCase, private val toggleReaction: ToggleReactionUseCase, private val resetSession: ResetSessionUseCase, - private val conversationAudioMessagePlayer: ConversationAudioMessagePlayer, + private val conversationAudioMessagePlayerProvider: ConversationAudioMessagePlayerProvider, private val getConversationUnreadEventsCount: GetConversationUnreadEventsCountUseCase, private val clearUsersTypingEvents: ClearUsersTypingEventsUseCase, private val getSearchedConversationMessagePosition: GetSearchedConversationMessagePositionUseCase, @@ -108,6 +108,7 @@ class ConversationMessagesViewModel @Inject constructor( private val conversationNavArgs: ConversationNavArgs = savedStateHandle.navArgs() val conversationId: QualifiedID = conversationNavArgs.conversationId private val searchedMessageIdNavArgs: String? = conversationNavArgs.searchedMessageId + private val conversationAudioMessagePlayer = conversationAudioMessagePlayerProvider.provide() var conversationViewState by mutableStateOf( ConversationMessagesViewState( @@ -436,7 +437,7 @@ class ConversationMessagesViewModel @Inject constructor( override fun onCleared() { super.onCleared() - conversationAudioMessagePlayer.close() + conversationAudioMessagePlayerProvider.onCleared() } private companion object { diff --git a/app/src/test/kotlin/com/wire/android/media/ConversationAudioMessagePlayerTest.kt b/app/src/test/kotlin/com/wire/android/media/ConversationAudioMessagePlayerTest.kt index 57c51fcb7c9..d761d47ca4c 100644 --- a/app/src/test/kotlin/com/wire/android/media/ConversationAudioMessagePlayerTest.kt +++ b/app/src/test/kotlin/com/wire/android/media/ConversationAudioMessagePlayerTest.kt @@ -25,9 +25,12 @@ import com.wire.android.framework.FakeKaliumFileSystem import com.wire.android.media.audiomessage.AudioMediaPlayingState import com.wire.android.media.audiomessage.AudioState import com.wire.android.media.audiomessage.ConversationAudioMessagePlayer +import com.wire.kalium.logic.CoreLogic +import com.wire.kalium.logic.data.auth.AccountInfo import com.wire.kalium.logic.data.id.ConversationId -import com.wire.kalium.logic.feature.asset.GetMessageAssetUseCase +import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.feature.asset.MessageAssetResult +import com.wire.kalium.logic.feature.session.CurrentSessionResult import io.mockk.MockKAnnotations import io.mockk.coEvery import io.mockk.every @@ -45,11 +48,14 @@ class ConversationAudioMessagePlayerTest { val (arrangement, conversationAudioMessagePlayer) = Arrangement() .withAudioMediaPlayerReturningTotalTime(1000) .withSuccessFullAssetFetch() + .withCurrentSession() .arrange() val testAudioMessageId = "some-dummy-message-id" conversationAudioMessagePlayer.observableAudioMessagesState.test { + // skip first emit from onStart + awaitItem() conversationAudioMessagePlayer.playAudio( ConversationId("some-dummy-value", "some.dummy.domain"), testAudioMessageId @@ -95,6 +101,7 @@ class ConversationAudioMessagePlayerTest { fun givenTheSuccessFullAssetFetch_whenPlayingTheSameMessageIdTwiceSequentially_thenEmitStatesAsExpected() = runTest { val (arrangement, conversationAudioMessagePlayer) = Arrangement() .withSuccessFullAssetFetch() + .withCurrentSession() .withAudioMediaPlayerReturningTotalTime(1000) .withMediaPlayerPlaying() .arrange() @@ -102,6 +109,8 @@ class ConversationAudioMessagePlayerTest { val testAudioMessageId = "some-dummy-message-id" conversationAudioMessagePlayer.observableAudioMessagesState.test { + // skip first emit from onStart + awaitItem() // playing first time conversationAudioMessagePlayer.playAudio( ConversationId("some-dummy-value", "some.dummy.domain"), @@ -161,6 +170,7 @@ class ConversationAudioMessagePlayerTest { runTest { val (arrangement, conversationAudioMessagePlayer) = Arrangement() .withSuccessFullAssetFetch() + .withCurrentSession() .withAudioMediaPlayerReturningTotalTime(1000) .arrange() @@ -168,6 +178,8 @@ class ConversationAudioMessagePlayerTest { val secondAudioMessageId = "some-dummy-message-id2" conversationAudioMessagePlayer.observableAudioMessagesState.test { + // skip first emit from onStart + awaitItem() // playing first audio message conversationAudioMessagePlayer.playAudio( ConversationId("some-dummy-value", "some.dummy.domain"), @@ -242,6 +254,7 @@ class ConversationAudioMessagePlayerTest { runTest { val (arrangement, conversationAudioMessagePlayer) = Arrangement() .withSuccessFullAssetFetch() + .withCurrentSession() .withAudioMediaPlayerReturningTotalTime(1000) .arrange() @@ -249,6 +262,8 @@ class ConversationAudioMessagePlayerTest { val secondAudioMessageId = "some-dummy-message-id2" conversationAudioMessagePlayer.observableAudioMessagesState.test { + // skip first emit from onStart + awaitItem() // playing first audio message conversationAudioMessagePlayer.playAudio( ConversationId("some-dummy-value", "some.dummy.domain"), @@ -366,6 +381,7 @@ class ConversationAudioMessagePlayerTest { runTest { val (arrangement, conversationAudioMessagePlayer) = Arrangement() .withSuccessFullAssetFetch() + .withCurrentSession() .withAudioMediaPlayerReturningTotalTime(1000) .withMediaPlayerPlaying() .arrange() @@ -373,6 +389,8 @@ class ConversationAudioMessagePlayerTest { val testAudioMessageId = "some-dummy-message-id" conversationAudioMessagePlayer.observableAudioMessagesState.test { + // skip first emit from onStart + awaitItem() // playing first time conversationAudioMessagePlayer.playAudio( ConversationId("some-dummy-value", "some.dummy.domain"), @@ -454,7 +472,7 @@ class Arrangement { lateinit var context: Context @MockK - lateinit var getMessageAssetUseCase: GetMessageAssetUseCase + lateinit var coreLogic: CoreLogic @MockK lateinit var mediaPlayer: MediaPlayer @@ -463,7 +481,7 @@ class Arrangement { ConversationAudioMessagePlayer( context, mediaPlayer, - getMessageAssetUseCase, + coreLogic, ) } @@ -471,8 +489,16 @@ class Arrangement { MockKAnnotations.init(this, relaxed = true) } + fun withCurrentSession() = apply { + coEvery { coreLogic.getGlobalScope().session.currentSession.invoke() } returns CurrentSessionResult.Success( + AccountInfo.Valid(UserId("some-user-value", "some.user.domain")) + ) + } + fun withSuccessFullAssetFetch() = apply { - coEvery { getMessageAssetUseCase.invoke(any(), any()) } returns CompletableDeferred( + coEvery { + coreLogic.getSessionScope(any()).messages.getAssetMessage.invoke(any(), any()) + } returns CompletableDeferred( MessageAssetResult.Success( decodedAssetPath = FakeKaliumFileSystem().selfUserAvatarPath(), assetSize = 0, diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/messages/ConversationMessagesViewModelArrangement.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/messages/ConversationMessagesViewModelArrangement.kt index 491c2fd2e56..bc48d1c4fe4 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/conversations/messages/ConversationMessagesViewModelArrangement.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/messages/ConversationMessagesViewModelArrangement.kt @@ -24,6 +24,7 @@ import com.wire.android.config.TestDispatcherProvider import com.wire.android.config.mockUri import com.wire.android.media.audiomessage.AudioState import com.wire.android.media.audiomessage.ConversationAudioMessagePlayer +import com.wire.android.media.audiomessage.ConversationAudioMessagePlayerProvider import com.wire.android.ui.home.conversations.ConversationNavArgs import com.wire.android.ui.home.conversations.model.AssetBundle import com.wire.android.ui.home.conversations.model.UIMessage @@ -96,6 +97,9 @@ class ConversationMessagesViewModelArrangement { @MockK lateinit var conversationAudioMessagePlayer: ConversationAudioMessagePlayer + @MockK + lateinit var conversationAudioMessagePlayerProvider: ConversationAudioMessagePlayerProvider + @MockK lateinit var getConversationUnreadEventsCount: GetConversationUnreadEventsCountUseCase @@ -124,7 +128,7 @@ class ConversationMessagesViewModelArrangement { getMessagesForConversationUseCase, toggleReaction, resetSession, - conversationAudioMessagePlayer, + conversationAudioMessagePlayerProvider, getConversationUnreadEventsCount, clearUsersTypingEvents, getSearchedConversationMessagePosition, @@ -143,6 +147,8 @@ class ConversationMessagesViewModelArrangement { coEvery { getConversationUnreadEventsCount(any()) } returns GetConversationUnreadEventsCountUseCase.Result.Success(0L) coEvery { updateAssetMessageDownloadStatus(any(), any(), any()) } returns UpdateTransferStatusResult.Success coEvery { clearUsersTypingEvents() } returns Unit + every { conversationAudioMessagePlayerProvider.provide() } returns conversationAudioMessagePlayer + every { conversationAudioMessagePlayerProvider.onCleared() } returns Unit coEvery { getSearchedConversationMessagePosition(any(), any()) } returns GetSearchedConversationMessagePositionUseCase.Result.Success(position = 0) From 29830bda10ef7eb2de395b976cbb8868607ebeda Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 26 Nov 2024 11:41:20 +0100 Subject: [PATCH 4/4] chore(deps): bump dawidd6/action-download-artifact from 4 to 6 in /.github/workflows (#3661) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/publish-test-results.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish-test-results.yml b/.github/workflows/publish-test-results.yml index c8ecbf005f9..f1291ea7a59 100644 --- a/.github/workflows/publish-test-results.yml +++ b/.github/workflows/publish-test-results.yml @@ -36,7 +36,7 @@ jobs: uses: actions/checkout@v4 - name: Download and Extract Artifacts - uses: dawidd6/action-download-artifact@v4 + uses: dawidd6/action-download-artifact@v6 with: run_id: ${{ github.event.workflow_run.id }} path: artifacts