From 6d3d16f562eec6e17e4313ac34ee838cee5a9fc9 Mon Sep 17 00:00:00 2001 From: Jakub Zerko Date: Fri, 24 May 2024 12:39:43 +0200 Subject: [PATCH] fix: bottom sheet state --- .../android/navigation/HomeDestination.kt | 30 +- .../com/wire/android/ui/home/HomeScreen.kt | 156 ++-------- .../android/ui/home/HomeSnackBarMessage.kt | 81 +++-- .../wire/android/ui/home/HomeStateHolder.kt | 55 +--- .../android/ui/home/archive/ArchiveScreen.kt | 5 - .../details/GroupConversationDetailsScreen.kt | 37 ++- .../media/ConversationMediaScreen.kt | 21 +- .../ConversationCallListViewModel.kt | 73 +---- .../ConversationListViewModel.kt | 81 ++--- .../conversationslist/ConversationRouter.kt | 236 +++++++-------- .../all/AllConversationScreen.kt | 50 ++-- .../android/util/ui/SnackBarMessageHandler.kt | 37 +++ .../ConversationCallListViewModelTest.kt | 281 ++---------------- .../ConversationListViewModelTest.kt | 160 ---------- .../bottomsheet/WireModalSheetLayout.kt | 42 +++ 15 files changed, 387 insertions(+), 958 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/navigation/HomeDestination.kt b/app/src/main/kotlin/com/wire/android/navigation/HomeDestination.kt index 551d77f1c76..4baac30ed2f 100644 --- a/app/src/main/kotlin/com/wire/android/navigation/HomeDestination.kt +++ b/app/src/main/kotlin/com/wire/android/navigation/HomeDestination.kt @@ -22,11 +22,8 @@ import androidx.annotation.DrawableRes import androidx.annotation.StringRes import com.ramcosta.composedestinations.spec.Direction import com.wire.android.R -import com.wire.android.ui.common.WireBottomNavigationItemData import com.wire.android.ui.destinations.AllConversationScreenDestination import com.wire.android.ui.destinations.ArchiveScreenDestination -import com.wire.android.ui.destinations.CallsScreenDestination -import com.wire.android.ui.destinations.MentionScreenDestination import com.wire.android.ui.destinations.SettingsScreenDestination import com.wire.android.ui.destinations.VaultScreenDestination import com.wire.android.ui.destinations.WhatsNewScreenDestination @@ -49,22 +46,6 @@ sealed class HomeDestination( direction = AllConversationScreenDestination ) - data object Calls : HomeDestination( - title = R.string.conversations_calls_tab_title, - icon = R.drawable.ic_call, - isSearchable = true, - withNewConversationFab = true, - direction = CallsScreenDestination - ) - - data object Mentions : HomeDestination( - title = R.string.conversations_mentions_tab_title, - icon = R.drawable.ic_mention, - isSearchable = true, - withNewConversationFab = true, - direction = MentionScreenDestination - ) - data object Settings : HomeDestination( title = R.string.settings_screen_title, icon = R.drawable.ic_settings, @@ -96,22 +77,13 @@ sealed class HomeDestination( direction = WhatsNewScreenDestination ) - val withBottomTabs: Boolean get() = bottomTabItems.contains(this) - - fun toBottomNavigationItemData(notificationAmount: Long): WireBottomNavigationItemData = - WireBottomNavigationItemData(icon, tabName, notificationAmount, direction.route) - val itemName: String get() = ITEM_NAME_PREFIX + this companion object { - // TODO uncomment when CallsScreen and MentionScreen will be implemented -// val bottomTabItems = listOf(Conversations, Calls, Mentions) - val bottomTabItems = listOf() - private const val ITEM_NAME_PREFIX = "HomeNavigationItem." fun fromRoute(fullRoute: String): HomeDestination? = values().find { it.direction.route.getBaseRoute() == fullRoute.getBaseRoute() } fun values(): Array = - arrayOf(Conversations, Calls, Mentions, Settings, Vault, Archive, Support, WhatsNew) + arrayOf(Conversations, Settings, Vault, Archive, Support, WhatsNew) } } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/HomeScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/HomeScreen.kt index 798dc5db0e0..d85144aec91 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/HomeScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/HomeScreen.kt @@ -24,8 +24,6 @@ import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.animateContentSize import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut -import androidx.compose.animation.slideInVertically -import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.Image import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column @@ -36,11 +34,11 @@ import androidx.compose.material3.DrawerDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalDrawerSheet import androidx.compose.material3.ModalNavigationDrawer -import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.RectangleShape @@ -72,10 +70,8 @@ import com.wire.android.navigation.handleNavigation import com.wire.android.ui.NavGraphs import com.wire.android.ui.common.CollapsingTopBarScaffold import com.wire.android.ui.common.FloatingActionButton -import com.wire.android.ui.common.WireBottomNavigationBar -import com.wire.android.ui.common.WireBottomNavigationItemData -import com.wire.android.ui.common.bottomsheet.WireModalSheetLayout import com.wire.android.ui.common.dimensions +import com.wire.android.ui.common.snackbar.LocalSnackbarHostState import com.wire.android.ui.common.topappbar.search.SearchTopBar import com.wire.android.ui.destinations.ConversationScreenDestination import com.wire.android.ui.destinations.NewConversationSearchPeopleScreenDestination @@ -83,9 +79,6 @@ import com.wire.android.ui.destinations.OtherUserProfileScreenDestination import com.wire.android.ui.destinations.SelfUserProfileScreenDestination import com.wire.android.ui.home.conversations.details.GroupConversationActionType import com.wire.android.ui.home.conversations.details.GroupConversationDetailsNavBackArgs -import com.wire.android.ui.home.conversationslist.ConversationListState -import com.wire.android.ui.home.conversationslist.ConversationListViewModel -import com.wire.android.ui.home.conversationslist.model.ConversationsSource import com.wire.android.ui.home.drawer.HomeDrawer import com.wire.android.ui.home.drawer.HomeDrawerState import com.wire.android.ui.home.drawer.HomeDrawerViewModel @@ -97,19 +90,21 @@ import kotlinx.coroutines.launch @Composable fun HomeScreen( navigator: Navigator, - homeViewModel: HomeViewModel = hiltViewModel(), - appSyncViewModel: AppSyncViewModel = hiltViewModel(), - homeDrawerViewModel: HomeDrawerViewModel = hiltViewModel(), - conversationListViewModel: ConversationListViewModel = hiltViewModel(), // TODO: move required elements from this one to HomeViewModel?, groupDetailsScreenResultRecipient: ResultRecipient, otherUserProfileScreenResultRecipient: ResultRecipient, + homeViewModel: HomeViewModel = hiltViewModel(), + appSyncViewModel: AppSyncViewModel = hiltViewModel(), + homeDrawerViewModel: HomeDrawerViewModel = hiltViewModel() ) { - homeViewModel.checkRequirements() { it.navigate(navigator::navigate) } + homeViewModel.checkRequirements { it.navigate(navigator::navigate) } val homeScreenState = rememberHomeScreenState(navigator) val showNotificationsFlow = rememberRequestPushNotificationsPermissionFlow( onPermissionDenied = { /** TODO: Show a dialog rationale explaining why the permission is needed **/ }) val lifecycleOwner = LocalLifecycleOwner.current + val context = LocalContext.current + val snackbarHostState = LocalSnackbarHostState.current + val coroutineScope = rememberCoroutineScope() DisposableEffect(lifecycleOwner) { val observer = LifecycleEventObserver { _, event -> @@ -128,20 +123,6 @@ fun HomeScreen( showNotificationsFlow.launch() } - LaunchedEffect(homeScreenState.currentNavigationItem) { - when (homeScreenState.currentNavigationItem) { - HomeDestination.Archive -> conversationListViewModel.updateConversationsSource(ConversationsSource.ARCHIVE) - HomeDestination.Conversations -> conversationListViewModel.updateConversationsSource(ConversationsSource.MAIN) - else -> {} - } - } - - handleSnackBarMessage( - snackbarHostState = homeScreenState.snackBarHostState, - conversationListSnackBarState = homeScreenState.snackbarState, - onMessageShown = homeScreenState::clearSnackbarMessage - ) - val homeState = homeViewModel.homeState if (homeViewModel.homeState.shouldDisplayWelcomeMessage) { WelcomeNewUserDialog( @@ -153,7 +134,6 @@ fun HomeScreen( homeState = homeState, homeDrawerState = homeDrawerViewModel.drawerState, homeStateHolder = homeScreenState, - conversationListState = conversationListViewModel.conversationListState, onNewConversationClick = { navigator.navigate(NavigationCommand(NewConversationSearchPeopleScreenDestination)) }, onSelfUserClick = remember(navigator) { { navigator.navigate(NavigationCommand(SelfUserProfileScreenDestination)) } } ) @@ -176,12 +156,19 @@ fun HomeScreen( is NavResult.Value -> { when (result.value.groupConversationActionType) { GroupConversationActionType.LEAVE_GROUP -> { - homeScreenState.setSnackBarState(HomeSnackbarState.LeftConversationSuccess) + coroutineScope.launch { + snackbarHostState.showSnackbar((HomeSnackBarMessage.LeftConversationSuccess.uiText.asString(context.resources))) + } } GroupConversationActionType.DELETE_GROUP -> { - val groupDeletedSnackBar = HomeSnackbarState.DeletedConversationGroupSuccess(result.value.conversationName) - homeScreenState.setSnackBarState(groupDeletedSnackBar) + coroutineScope.launch { + snackbarHostState.showSnackbar( + HomeSnackBarMessage.DeletedConversationGroupSuccess(result.value.conversationName).uiText.asString( + context.resources + ) + ) + } } } } @@ -195,7 +182,11 @@ fun HomeScreen( } is NavResult.Value -> { - homeScreenState.setSnackBarState(HomeSnackbarState.SuccessConnectionIgnoreRequest(result.value)) + coroutineScope.launch { + snackbarHostState.showSnackbar( + HomeSnackBarMessage.SuccessConnectionIgnoreRequest(result.value).uiText.asString(context.resources) + ) + } } } } @@ -207,7 +198,6 @@ fun HomeContent( homeState: HomeState, homeDrawerState: HomeDrawerState, homeStateHolder: HomeStateHolder, - conversationListState: ConversationListState, onNewConversationClick: () -> Unit, onSelfUserClick: () -> Unit, ) { @@ -330,107 +320,9 @@ fun HomeContent( onClick = onNewConversationClick ) } - }, - bottomBar = { - AnimatedVisibility( - visible = currentNavigationItem.withBottomTabs, - enter = slideInVertically(initialOffsetY = { it }), - exit = slideOutVertically(targetOffsetY = { it }), - ) { - WireBottomNavigationBar( - items = HomeDestination.bottomTabItems.toBottomNavigationItemData( - conversationListState = conversationListState - ), - selectedItemRoute = homeStateHolder.currentNavigationItem.direction.route, - onItemSelected = { HomeDestination.fromRoute(it.route)?.let { openHomeDestination(it) } } - ) - } } ) - - WireModalSheetLayout( - sheetState = bottomSheetState, - coroutineScope = coroutineScope, - // we want to render "nothing" instead of doing a if/else check - // on homeBottomSheetContent and wrap homeContent() into WireModalSheetLayout - // or render it without WireModalSheetLayout to avoid - // recomposing the homeContent() when homeBottomSheetContent - // changes from null to "something" - sheetContent = homeBottomSheetContent ?: { } - ) } ) } } - -@Suppress("ComplexMethod") -@Composable -private fun handleSnackBarMessage( - snackbarHostState: SnackbarHostState, - conversationListSnackBarState: HomeSnackbarState, - onMessageShown: () -> Unit -) { - conversationListSnackBarState.let { messageType -> - val message = when (messageType) { - is HomeSnackbarState.SuccessConnectionIgnoreRequest -> - stringResource(id = R.string.connection_request_ignored, messageType.userName) - - is HomeSnackbarState.BlockingUserOperationSuccess -> - stringResource(id = R.string.blocking_user_success, messageType.userName) - - HomeSnackbarState.MutingOperationError -> stringResource(id = R.string.error_updating_muting_setting) - HomeSnackbarState.BlockingUserOperationError -> stringResource(id = R.string.error_blocking_user) - HomeSnackbarState.UnblockingUserOperationError -> stringResource(id = R.string.error_unblocking_user) - HomeSnackbarState.None -> "" - is HomeSnackbarState.DeletedConversationGroupSuccess -> stringResource( - id = R.string.conversation_group_removed_success, - messageType.groupName - ) - - HomeSnackbarState.LeftConversationSuccess -> stringResource(id = R.string.left_conversation_group_success) - HomeSnackbarState.LeaveConversationError -> stringResource(id = R.string.leave_group_conversation_error) - HomeSnackbarState.DeleteConversationGroupError -> stringResource(id = R.string.delete_group_conversation_error) - is HomeSnackbarState.ClearConversationContentFailure -> stringResource( - if (messageType.isGroup) R.string.group_content_delete_failure - else R.string.conversation_content_delete_failure - ) - - is HomeSnackbarState.ClearConversationContentSuccess -> stringResource( - if (messageType.isGroup) R.string.group_content_deleted else R.string.conversation_content_deleted - ) - - is HomeSnackbarState.UpdateArchivingStatusSuccess -> { - stringResource( - id = if (messageType.isArchiving) R.string.success_archiving_conversation - else R.string.success_unarchiving_conversation - ) - } - - is HomeSnackbarState.UpdateArchivingStatusError -> { - stringResource( - id = if (messageType.isArchiving) R.string.error_archiving_conversation - else R.string.error_archiving_conversation - ) - } - } - - LaunchedEffect(messageType) { - if (messageType != HomeSnackbarState.None) { - snackbarHostState.showSnackbar(message) - onMessageShown() - } - } - } -} - -@Composable -private fun List.toBottomNavigationItemData( - conversationListState: ConversationListState -): List = map { - when (it) { - HomeDestination.Conversations -> it.toBottomNavigationItemData(conversationListState.newActivityCount) - HomeDestination.Calls -> it.toBottomNavigationItemData(conversationListState.missedCallsCount) - HomeDestination.Mentions -> it.toBottomNavigationItemData(conversationListState.unreadMentionsCount) - else -> it.toBottomNavigationItemData(0L) - } -} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/HomeSnackBarMessage.kt b/app/src/main/kotlin/com/wire/android/ui/home/HomeSnackBarMessage.kt index b291bc54376..0a8bb081457 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/HomeSnackBarMessage.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/HomeSnackBarMessage.kt @@ -18,21 +18,68 @@ package com.wire.android.ui.home -// TODO change to extend [SnackBarMessage] -sealed class HomeSnackbarState { - object None : HomeSnackbarState() - data class ClearConversationContentSuccess(val isGroup: Boolean) : HomeSnackbarState() - data class ClearConversationContentFailure(val isGroup: Boolean) : HomeSnackbarState() - - class SuccessConnectionIgnoreRequest(val userName: String) : HomeSnackbarState() - object MutingOperationError : HomeSnackbarState() - object BlockingUserOperationError : HomeSnackbarState() - data class BlockingUserOperationSuccess(val userName: String) : HomeSnackbarState() - object UnblockingUserOperationError : HomeSnackbarState() - data class DeletedConversationGroupSuccess(val groupName: String) : HomeSnackbarState() - object DeleteConversationGroupError : HomeSnackbarState() - object LeftConversationSuccess : HomeSnackbarState() - object LeaveConversationError : HomeSnackbarState() - data class UpdateArchivingStatusSuccess(val isArchiving: Boolean) : HomeSnackbarState() - data class UpdateArchivingStatusError(val isArchiving: Boolean) : HomeSnackbarState() +import com.wire.android.R +import com.wire.android.model.SnackBarMessage +import com.wire.android.util.ui.UIText + +sealed class HomeSnackBarMessage(override val uiText: UIText) : SnackBarMessage { + + data class ClearConversationContentSuccess(val isGroup: Boolean) : HomeSnackBarMessage( + UIText.StringResource( + if (isGroup) { + R.string.group_content_deleted + } else { + R.string.conversation_content_deleted + } + ) + ) + + data class ClearConversationContentFailure(val isGroup: Boolean) : HomeSnackBarMessage( + UIText.StringResource( + if (isGroup) { + R.string.group_content_delete_failure + } else { + R.string.conversation_content_delete_failure + } + ) + ) + + class SuccessConnectionIgnoreRequest(val userName: String) : + HomeSnackBarMessage(UIText.StringResource(R.string.connection_request_ignored, userName)) + + data object MutingOperationError : HomeSnackBarMessage(UIText.StringResource(R.string.error_updating_muting_setting)) + data object BlockingUserOperationError : HomeSnackBarMessage(UIText.StringResource(R.string.error_blocking_user)) + data class BlockingUserOperationSuccess(val userName: String) : + HomeSnackBarMessage(UIText.StringResource(R.string.blocking_user_success, userName)) + + data object UnblockingUserOperationError : HomeSnackBarMessage(UIText.StringResource(R.string.error_unblocking_user)) + data class DeletedConversationGroupSuccess(val groupName: String) : HomeSnackBarMessage( + UIText.StringResource( + R.string.conversation_group_removed_success, + groupName + ) + ) + + data object DeleteConversationGroupError : HomeSnackBarMessage(UIText.StringResource(R.string.delete_group_conversation_error)) + data object LeftConversationSuccess : HomeSnackBarMessage(UIText.StringResource(R.string.left_conversation_group_success)) + data object LeaveConversationError : HomeSnackBarMessage(UIText.StringResource(R.string.leave_group_conversation_error)) + data class UpdateArchivingStatusSuccess(val isArchiving: Boolean) : HomeSnackBarMessage( + UIText.StringResource( + if (isArchiving) { + R.string.success_archiving_conversation + } else { + R.string.success_unarchiving_conversation + } + ) + ) + + data class UpdateArchivingStatusError(val isArchiving: Boolean) : HomeSnackBarMessage( + UIText.StringResource( + if (isArchiving) { + R.string.error_archiving_conversation + } else { + R.string.error_archiving_conversation + } + ) + ) } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/HomeStateHolder.kt b/app/src/main/kotlin/com/wire/android/ui/home/HomeStateHolder.kt index f84b680a355..68616554be7 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/HomeStateHolder.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/HomeStateHolder.kt @@ -16,33 +16,22 @@ * along with this program. If not, see http://www.gnu.org/licenses/. */ -@file:OptIn(ExperimentalAnimationApi::class) - package com.wire.android.ui.home -import androidx.compose.animation.ExperimentalAnimationApi -import androidx.compose.foundation.layout.ColumnScope import androidx.compose.material3.DrawerState import androidx.compose.material3.DrawerValue -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.rememberDrawerState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue import androidx.navigation.NavHostController import androidx.navigation.compose.currentBackStackEntryAsState import com.wire.android.navigation.HomeDestination import com.wire.android.navigation.Navigator import com.wire.android.navigation.rememberTrackingAnimatedNavController -import com.wire.android.ui.common.bottomsheet.WireModalSheetState -import com.wire.android.ui.common.bottomsheet.rememberWireModalSheetState import com.wire.android.ui.common.topappbar.search.SearchBarState import com.wire.android.ui.common.topappbar.search.rememberSearchbarState -import com.wire.android.ui.common.snackbar.LocalSnackbarHostState import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -51,46 +40,11 @@ class HomeStateHolder( val coroutineScope: CoroutineScope, val navController: NavHostController, val drawerState: DrawerState, - val bottomSheetState: WireModalSheetState, val currentNavigationItem: HomeDestination, - val snackBarHostState: SnackbarHostState, val searchBarState: SearchBarState, val navigator: Navigator ) { - var homeBottomSheetContent: @Composable (ColumnScope.() -> Unit)? by mutableStateOf(null) - private set - - var snackbarState: HomeSnackbarState by mutableStateOf(HomeSnackbarState.None) - private set - - fun setSnackBarState(state: HomeSnackbarState) { - snackbarState = state - if (state != HomeSnackbarState.None) closeBottomSheet() - } - - fun clearSnackbarMessage() { - setSnackBarState(HomeSnackbarState.None) - } - - fun openBottomSheet() { - coroutineScope.launch { - if (!bottomSheetState.isVisible) bottomSheetState.show() - } - } - - fun closeBottomSheet() { - coroutineScope.launch { - if (bottomSheetState.isVisible) bottomSheetState.hide() - } - } - - fun isBottomSheetVisible() = bottomSheetState.isVisible - - fun changeBottomSheetContent(content: @Composable ColumnScope.() -> Unit) { - homeBottomSheetContent = content - } - fun closeDrawer() { coroutineScope.launch { drawerState.close() @@ -104,16 +58,15 @@ class HomeStateHolder( } } -@OptIn(ExperimentalMaterial3Api::class) @Composable fun rememberHomeScreenState( navigator: Navigator, coroutineScope: CoroutineScope = rememberCoroutineScope(), - navController: NavHostController = rememberTrackingAnimatedNavController() { HomeDestination.fromRoute(it)?.itemName }, + navController: NavHostController = rememberTrackingAnimatedNavController { + HomeDestination.fromRoute(it)?.itemName + }, drawerState: DrawerState = rememberDrawerState(DrawerValue.Closed), - bottomSheetState: WireModalSheetState = rememberWireModalSheetState() ): HomeStateHolder { - val snackbarHostState = LocalSnackbarHostState.current val navBackStackEntry by navController.currentBackStackEntryAsState() val currentRoute = navBackStackEntry?.destination?.route val currentNavigationItem = currentRoute?.let { HomeDestination.fromRoute(it) } ?: HomeDestination.Conversations @@ -127,9 +80,7 @@ fun rememberHomeScreenState( coroutineScope, navController, drawerState, - bottomSheetState, currentNavigationItem, - snackbarHostState, searchBarState, navigator ) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/archive/ArchiveScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/archive/ArchiveScreen.kt index fb6491bee42..7c08265ef09 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/archive/ArchiveScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/archive/ArchiveScreen.kt @@ -60,12 +60,7 @@ fun ArchiveScreen(homeStateHolder: HomeStateHolder) { ConversationRouterHomeBridge( navigator = navigator, conversationItemType = ConversationItemType.ALL_CONVERSATIONS, - onHomeBottomSheetContentChanged = ::changeBottomSheetContent, - onOpenBottomSheet = ::openBottomSheet, - onCloseBottomSheet = ::closeBottomSheet, - onSnackBarStateChanged = ::setSnackBarState, searchBarState = searchBarState, - isBottomSheetVisible = ::isBottomSheetVisible, conversationsSource = ConversationsSource.ARCHIVE ) } 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 d2ffb0e7076..fe2af7e51a5 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 @@ -36,7 +36,6 @@ import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState -import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface @@ -58,7 +57,6 @@ import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview import androidx.hilt.navigation.compose.hiltViewModel import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.RootNavGraph @@ -79,6 +77,7 @@ 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.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.rememberWireModalSheetState import com.wire.android.ui.common.button.WirePrimaryButton @@ -116,9 +115,14 @@ import com.wire.android.ui.theme.WireTheme import com.wire.android.ui.theme.wireColorScheme import com.wire.android.ui.theme.wireDimensions import com.wire.android.ui.theme.wireTypography +import com.wire.android.util.ui.PreviewMultipleThemes import com.wire.android.util.ui.UIText import com.wire.kalium.logic.data.conversation.Conversation +import com.wire.kalium.logic.data.conversation.MutedConversationStatus +import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.data.id.GroupID import kotlinx.coroutines.launch +import kotlinx.datetime.Instant @RootNavGraph @Destination( @@ -128,9 +132,9 @@ import kotlinx.coroutines.launch @Composable fun GroupConversationDetailsScreen( navigator: Navigator, - viewModel: GroupConversationDetailsViewModel = hiltViewModel(), resultNavigator: ResultBackNavigator, groupConversationDetailResultRecipient: ResultRecipient, + viewModel: GroupConversationDetailsViewModel = hiltViewModel() ) { val scope = rememberCoroutineScope() val resources = LocalContext.current.resources @@ -159,7 +163,7 @@ fun GroupConversationDetailsScreen( GroupConversationDetailsContent( conversationSheetContent = viewModel.conversationSheetContent, - bottomSheetEventsHandler = viewModel, + bottomSheetEventsHandler = viewModel as GroupConversationDetailsBottomSheetEventsHandler, onBackPressed = navigator::navigateBack, onProfilePressed = { participant -> when { @@ -260,7 +264,7 @@ fun GroupConversationDetailsScreen( } } -@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class, ExperimentalMaterialApi::class) +@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) @Composable private fun GroupConversationDetailsContent( conversationSheetContent: ConversationSheetContent?, @@ -561,15 +565,34 @@ private fun VerifiedLabel(text: String, color: Color, icon: @Composable RowScope enum class GroupConversationDetailsTabItem(@StringRes val titleResId: Int) : TabItem { OPTIONS(R.string.conversation_details_options_tab), PARTICIPANTS(R.string.conversation_details_participants_tab); + override val title: UIText = UIText.StringResource(titleResId) } -@Preview +@PreviewMultipleThemes @Composable fun PreviewGroupConversationDetails() { WireTheme { GroupConversationDetailsContent( - conversationSheetContent = null, + conversationSheetContent = ConversationSheetContent( + title = "title", + conversationId = ConversationId("value", "domain"), + mutingConversationState = MutedConversationStatus.AllAllowed, + conversationTypeDetail = ConversationTypeDetail.Group(ConversationId("value", "domain"), false), + selfRole = null, + isTeamConversation = true, + isArchived = false, + protocol = Conversation.ProtocolInfo.MLS( + groupId = GroupID("groupId"), + groupState = Conversation.ProtocolInfo.MLSCapable.GroupState.ESTABLISHED, + epoch = ULong.MIN_VALUE, + keyingMaterialLastUpdate = Instant.fromEpochMilliseconds(1648654560000), + cipherSuite = Conversation.CipherSuite.MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 + ), + mlsVerificationStatus = Conversation.VerificationStatus.VERIFIED, + isUnderLegalHold = false, + proteusVerificationStatus = Conversation.VerificationStatus.VERIFIED + ), bottomSheetEventsHandler = GroupConversationDetailsBottomSheetEventsHandler.PREVIEW, onBackPressed = {}, onProfilePressed = {}, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/ConversationMediaScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/ConversationMediaScreen.kt index 4395bfe2be1..600a2f223fa 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/ConversationMediaScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/ConversationMediaScreen.kt @@ -39,14 +39,12 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.hilt.navigation.compose.hiltViewModel import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.RootNavGraph import com.wire.android.R import com.wire.android.media.audiomessage.AudioState -import com.wire.android.model.SnackBarMessage import com.wire.android.navigation.NavigationCommand import com.wire.android.navigation.Navigator import com.wire.android.navigation.style.PopUpNavigationAnimation @@ -56,7 +54,6 @@ import com.wire.android.ui.common.calculateCurrentTab import com.wire.android.ui.common.colorsScheme import com.wire.android.ui.common.dialogs.PermissionPermanentlyDeniedDialog import com.wire.android.ui.common.scaffold.WireScaffold -import com.wire.android.ui.common.snackbar.LocalSnackbarHostState import com.wire.android.ui.common.topBarElevation import com.wire.android.ui.common.topappbar.NavigationIconType import com.wire.android.ui.common.topappbar.WireCenterAlignedTopAppBar @@ -68,11 +65,11 @@ import com.wire.android.ui.home.conversations.messages.ConversationMessagesViewM import com.wire.android.ui.theme.WireTheme import com.wire.android.ui.theme.wireDimensions 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 kotlinx.collections.immutable.PersistentMap import kotlinx.collections.immutable.persistentMapOf -import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.launch @RootNavGraph @@ -131,7 +128,7 @@ fun ConversationMediaScreen( hideDialog = permissionPermanentlyDeniedDialogState::dismiss ) - SnackBarMessage(conversationMessagesViewModel.infoMessage) + SnackBarMessageHandler(conversationMessagesViewModel.infoMessage) } @OptIn(ExperimentalFoundationApi::class) @@ -206,20 +203,6 @@ private fun Content( } } -@Composable -private fun SnackBarMessage(infoMessages: SharedFlow) { - val context = LocalContext.current - val snackbarHostState = LocalSnackbarHostState.current - - LaunchedEffect(Unit) { - infoMessages.collect { - snackbarHostState.showSnackbar( - message = it.uiText.asString(context.resources) - ) - } - } -} - enum class ConversationMediaScreenTabItem(@StringRes val titleResId: Int) : TabItem { PICTURES(R.string.label_conversation_pictures), FILES(R.string.label_conversation_files); diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationCallListViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationCallListViewModel.kt index 6851dfb9c1a..eadd68f7ba1 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationCallListViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationCallListViewModel.kt @@ -21,90 +21,29 @@ package com.wire.android.ui.home.conversationslist import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.compose.ui.text.input.TextFieldValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.wire.android.appLogger -import com.wire.android.mapper.UserTypeMapper -import com.wire.android.mapper.toUIPreview -import com.wire.android.model.ImageAsset.UserAvatarAsset import com.wire.android.model.SnackBarMessage -import com.wire.android.model.UserAvatarData -import com.wire.android.ui.common.bottomsheet.conversation.ConversationTypeDetail -import com.wire.android.ui.common.dialogs.BlockUserDialogState -import com.wire.android.ui.home.HomeSnackBarMessage -import com.wire.android.ui.home.conversations.model.UILastMessageContent -import com.wire.android.ui.home.conversations.search.DEFAULT_SEARCH_QUERY_DEBOUNCE -import com.wire.android.ui.home.conversationslist.model.BadgeEventType -import com.wire.android.ui.home.conversationslist.model.BlockState -import com.wire.android.ui.home.conversationslist.model.ConversationFolder -import com.wire.android.ui.home.conversationslist.model.ConversationInfo -import com.wire.android.ui.home.conversationslist.model.ConversationItem -import com.wire.android.ui.home.conversationslist.model.ConversationsSource -import com.wire.android.ui.home.conversationslist.model.DialogState -import com.wire.android.ui.home.conversationslist.model.GroupDialogState -import com.wire.android.ui.home.conversationslist.model.SearchQuery -import com.wire.android.ui.home.conversationslist.model.SearchQueryUpdate -import com.wire.android.util.dispatchers.DispatcherProvider -import com.wire.android.util.ui.WireSessionImageLoader -import com.wire.kalium.logic.data.conversation.Conversation -import com.wire.kalium.logic.data.conversation.ConversationDetails -import com.wire.kalium.logic.data.conversation.ConversationDetails.Connection -import com.wire.kalium.logic.data.conversation.ConversationDetails.Group -import com.wire.kalium.logic.data.conversation.ConversationDetails.OneOne -import com.wire.kalium.logic.data.conversation.ConversationDetails.Self -import com.wire.kalium.logic.data.conversation.MutedConversationStatus -import com.wire.kalium.logic.data.conversation.UnreadEventCount import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.id.QualifiedID -import com.wire.kalium.logic.data.message.UnreadEventType -import com.wire.kalium.logic.data.user.ConnectionState -import com.wire.kalium.logic.data.user.UserAvailabilityStatus -import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.feature.call.usecase.AnswerCallUseCase import com.wire.kalium.logic.feature.call.usecase.EndCallUseCase import com.wire.kalium.logic.feature.call.usecase.ObserveEstablishedCallsUseCase -import com.wire.kalium.logic.feature.connection.BlockUserResult -import com.wire.kalium.logic.feature.connection.BlockUserUseCase -import com.wire.kalium.logic.feature.connection.UnblockUserResult -import com.wire.kalium.logic.feature.connection.UnblockUserUseCase -import com.wire.kalium.logic.feature.conversation.ArchiveStatusUpdateResult -import com.wire.kalium.logic.feature.conversation.ClearConversationContentUseCase -import com.wire.kalium.logic.feature.conversation.ConversationUpdateStatusResult -import com.wire.kalium.logic.feature.conversation.LeaveConversationUseCase -import com.wire.kalium.logic.feature.conversation.ObserveConversationListDetailsUseCase -import com.wire.kalium.logic.feature.conversation.RefreshConversationsWithoutMetadataUseCase -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.publicuser.RefreshUsersWithoutMetadataUseCase -import com.wire.kalium.logic.feature.team.DeleteTeamConversationUseCase -import com.wire.kalium.logic.feature.team.Result -import com.wire.kalium.util.DateTimeUtil import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.collections.immutable.toImmutableMap import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.scan import kotlinx.coroutines.launch -import java.util.Date import javax.inject.Inject @Suppress("MagicNumber", "TooManyFunctions", "LongParameterList") @HiltViewModel -class ConversationListCallViewModel @Inject constructor( +class ConversationCallListViewModel @Inject constructor( private val answerCall: AnswerCallUseCase, private val observeEstablishedCalls: ObserveEstablishedCallsUseCase, - private val endCall: EndCallUseCase, - private val refreshUsersWithoutMetadata: RefreshUsersWithoutMetadataUseCase, - private val refreshConversationsWithoutMetadata: RefreshConversationsWithoutMetadataUseCase, + private val endCall: EndCallUseCase ) : ViewModel() { var conversationListCallState by mutableStateOf(ConversationListCallState()) @@ -133,18 +72,10 @@ class ConversationListCallViewModel @Inject constructor( init { viewModelScope.launch { - println("KBX observeEstablishedCall") observeEstablishedCall() } } - suspend fun refreshMissingMetadata() { - viewModelScope.launch { - refreshUsersWithoutMetadata() - refreshConversationsWithoutMetadata() - } - } - fun joinAnyway(conversationId: ConversationId, onJoined: (ConversationId) -> Unit) { viewModelScope.launch { establishedCallConversationId?.let { 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 345b2e2a654..7f053c23143 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 @@ -28,10 +28,11 @@ import com.wire.android.appLogger import com.wire.android.mapper.UserTypeMapper import com.wire.android.mapper.toUIPreview import com.wire.android.model.ImageAsset.UserAvatarAsset +import com.wire.android.model.SnackBarMessage import com.wire.android.model.UserAvatarData import com.wire.android.ui.common.bottomsheet.conversation.ConversationTypeDetail import com.wire.android.ui.common.dialogs.BlockUserDialogState -import com.wire.android.ui.home.HomeSnackbarState +import com.wire.android.ui.home.HomeSnackBarMessage import com.wire.android.ui.home.conversations.model.UILastMessageContent import com.wire.android.ui.home.conversations.search.DEFAULT_SEARCH_QUERY_DEBOUNCE import com.wire.android.ui.home.conversationslist.model.BadgeEventType @@ -60,8 +61,6 @@ import com.wire.kalium.logic.data.message.UnreadEventType import com.wire.kalium.logic.data.user.ConnectionState import com.wire.kalium.logic.data.user.UserAvailabilityStatus import com.wire.kalium.logic.data.user.UserId -import com.wire.kalium.logic.feature.call.usecase.AnswerCallUseCase -import com.wire.kalium.logic.feature.call.usecase.EndCallUseCase import com.wire.kalium.logic.feature.call.usecase.ObserveEstablishedCallsUseCase import com.wire.kalium.logic.feature.connection.BlockUserResult import com.wire.kalium.logic.feature.connection.BlockUserUseCase @@ -82,8 +81,8 @@ import com.wire.kalium.logic.feature.team.Result import com.wire.kalium.util.DateTimeUtil import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.collections.immutable.toImmutableMap -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.distinctUntilChanged @@ -100,7 +99,6 @@ import javax.inject.Inject class ConversationListViewModel @Inject constructor( private val dispatcher: DispatcherProvider, private val updateConversationMutedStatus: UpdateConversationMutedStatusUseCase, - private val answerCall: AnswerCallUseCase, private val observeConversationListDetails: ObserveConversationListDetailsUseCase, private val leaveConversation: LeaveConversationUseCase, private val deleteTeamConversation: DeleteTeamConversationUseCase, @@ -110,7 +108,6 @@ class ConversationListViewModel @Inject constructor( private val observeEstablishedCalls: ObserveEstablishedCallsUseCase, private val wireSessionImageLoader: WireSessionImageLoader, private val userTypeMapper: UserTypeMapper, - private val endCall: EndCallUseCase, private val refreshUsersWithoutMetadata: RefreshUsersWithoutMetadataUseCase, private val refreshConversationsWithoutMetadata: RefreshConversationsWithoutMetadataUseCase, private val updateConversationArchivedStatus: UpdateConversationArchivedStatusUseCase, @@ -119,7 +116,8 @@ class ConversationListViewModel @Inject constructor( var conversationListState by mutableStateOf(ConversationListState()) var conversationListCallState by mutableStateOf(ConversationListCallState()) - val homeSnackBarState = MutableSharedFlow() + private val _infoMessage = MutableSharedFlow() + val infoMessage = _infoMessage.asSharedFlow() val closeBottomSheet = MutableSharedFlow() @@ -309,8 +307,8 @@ class ConversationListViewModel @Inject constructor( mutedConversationStatus, Date().time )) { - ConversationUpdateStatusResult.Failure -> homeSnackBarState.emit( - HomeSnackbarState.MutingOperationError + ConversationUpdateStatusResult.Failure -> _infoMessage.emit( + HomeSnackBarMessage.MutingOperationError ) ConversationUpdateStatusResult.Success -> @@ -323,46 +321,13 @@ class ConversationListViewModel @Inject constructor( } } - fun joinAnyway(conversationId: ConversationId, onJoined: (ConversationId) -> Unit) { - viewModelScope.launch { - establishedCallConversationId?.let { - endCall(it) - delay(DELAY_END_CALL) - } - joinOngoingCall(conversationId, onJoined) - } - } - - fun joinOngoingCall(conversationId: ConversationId, onJoined: (ConversationId) -> Unit) { - this.conversationId = conversationId - if (conversationListCallState.hasEstablishedCall) { - showJoinCallAnywayDialog() - } else { - dismissJoinCallAnywayDialog() - viewModelScope.launch { - answerCall(conversationId = conversationId) - } - onJoined(conversationId) - } - } - - private fun showJoinCallAnywayDialog() { - conversationListCallState = - conversationListCallState.copy(shouldShowJoinAnywayDialog = true) - } - - fun dismissJoinCallAnywayDialog() { - conversationListCallState = - conversationListCallState.copy(shouldShowJoinAnywayDialog = false) - } - fun blockUser(blockUserState: BlockUserDialogState) { viewModelScope.launch { requestInProgress = true val state = when (val result = blockUserUseCase(blockUserState.userId)) { BlockUserResult.Success -> { appLogger.d("User ${blockUserState.userId} was blocked") - HomeSnackbarState.BlockingUserOperationSuccess(blockUserState.userName) + HomeSnackBarMessage.BlockingUserOperationSuccess(blockUserState.userName) } is BlockUserResult.Failure -> { @@ -370,10 +335,10 @@ class ConversationListViewModel @Inject constructor( "Error while blocking user ${blockUserState.userId} ;" + " Error ${result.coreFailure}" ) - HomeSnackbarState.BlockingUserOperationError + HomeSnackBarMessage.BlockingUserOperationError } } - homeSnackBarState.emit(state) + _infoMessage.emit(state) requestInProgress = false } } @@ -392,7 +357,7 @@ class ConversationListViewModel @Inject constructor( "Error while unblocking user $userId ;" + " Error ${result.coreFailure}" ) - homeSnackBarState.emit(HomeSnackbarState.UnblockingUserOperationError) + _infoMessage.emit(HomeSnackBarMessage.UnblockingUserOperationError) } } requestInProgress = false @@ -405,10 +370,10 @@ class ConversationListViewModel @Inject constructor( val response = leaveConversation(leaveGroupState.conversationId) when (response) { is RemoveMemberFromConversationUseCase.Result.Failure -> - homeSnackBarState.emit(HomeSnackbarState.LeaveConversationError) + _infoMessage.emit(HomeSnackBarMessage.LeaveConversationError) RemoveMemberFromConversationUseCase.Result.Success -> { - homeSnackBarState.emit(HomeSnackbarState.LeftConversationSuccess) + _infoMessage.emit(HomeSnackBarMessage.LeftConversationSuccess) } } requestInProgress = false @@ -419,10 +384,10 @@ class ConversationListViewModel @Inject constructor( viewModelScope.launch { requestInProgress = true when (deleteTeamConversation(groupDialogState.conversationId)) { - is Result.Failure.GenericFailure -> homeSnackBarState.emit(HomeSnackbarState.DeleteConversationGroupError) - Result.Failure.NoTeamFailure -> homeSnackBarState.emit(HomeSnackbarState.DeleteConversationGroupError) - Result.Success -> homeSnackBarState.emit( - HomeSnackbarState.DeletedConversationGroupSuccess(groupDialogState.conversationName) + is Result.Failure.GenericFailure -> _infoMessage.emit(HomeSnackBarMessage.DeleteConversationGroupError) + Result.Failure.NoTeamFailure -> _infoMessage.emit(HomeSnackBarMessage.DeleteConversationGroupError) + Result.Success -> _infoMessage.emit( + HomeSnackBarMessage.DeletedConversationGroupSuccess(groupDialogState.conversationName) ) } requestInProgress = false @@ -469,16 +434,16 @@ class ConversationListViewModel @Inject constructor( requestInProgress = false when (result) { is ArchiveStatusUpdateResult.Failure -> { - homeSnackBarState.emit( - HomeSnackbarState.UpdateArchivingStatusError( + _infoMessage.emit( + HomeSnackBarMessage.UpdateArchivingStatusError( isArchiving ) ) } is ArchiveStatusUpdateResult.Success -> { - homeSnackBarState.emit( - HomeSnackbarState.UpdateArchivingStatusSuccess( + _infoMessage.emit( + HomeSnackBarMessage.UpdateArchivingStatusSuccess( isArchiving ) ) @@ -510,9 +475,9 @@ class ConversationListViewModel @Inject constructor( val isGroup = conversationTypeDetail is ConversationTypeDetail.Group if (clearContentResult is ClearConversationContentUseCase.Result.Failure) { - homeSnackBarState.emit(HomeSnackbarState.ClearConversationContentFailure(isGroup)) + _infoMessage.emit(HomeSnackBarMessage.ClearConversationContentFailure(isGroup)) } else { - homeSnackBarState.emit(HomeSnackbarState.ClearConversationContentSuccess(isGroup)) + _infoMessage.emit(HomeSnackBarMessage.ClearConversationContentSuccess(isGroup)) } } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationRouter.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationRouter.kt index b8cdf494ab8..d297748fb44 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationRouter.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationRouter.kt @@ -19,12 +19,15 @@ package com.wire.android.ui.home.conversationslist import androidx.activity.compose.BackHandler -import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.SheetValue +import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.hilt.navigation.compose.hiltViewModel import com.wire.android.R @@ -32,6 +35,7 @@ import com.wire.android.navigation.NavigationCommand import com.wire.android.navigation.Navigator import com.wire.android.ui.LocalActivity import com.wire.android.ui.calling.getOngoingCallIntent +import com.wire.android.ui.common.bottomsheet.WireModalSheetLayout2 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 @@ -47,14 +51,11 @@ import com.wire.android.ui.common.visbility.rememberVisibilityState import com.wire.android.ui.destinations.ConversationScreenDestination import com.wire.android.ui.destinations.NewConversationSearchPeopleScreenDestination import com.wire.android.ui.destinations.OtherUserProfileScreenDestination -import com.wire.android.ui.home.HomeSnackbarState import com.wire.android.ui.home.conversations.PermissionPermanentlyDeniedDialogState import com.wire.android.ui.home.conversations.details.dialog.ClearConversationContentDialog import com.wire.android.ui.home.conversations.details.menu.DeleteConversationGroupDialog import com.wire.android.ui.home.conversations.details.menu.LeaveConversationGroupDialog import com.wire.android.ui.home.conversationslist.all.AllConversationScreenContent -import com.wire.android.ui.home.conversationslist.call.CallsScreenContent -import com.wire.android.ui.home.conversationslist.mention.MentionScreenContent import com.wire.android.ui.home.conversationslist.model.ConversationItem import com.wire.android.ui.home.conversationslist.model.ConversationsSource import com.wire.android.ui.home.conversationslist.model.DialogState @@ -62,6 +63,7 @@ import com.wire.android.ui.home.conversationslist.model.GroupDialogState import com.wire.android.ui.home.conversationslist.model.isArchive import com.wire.android.ui.home.conversationslist.search.SearchConversationScreen import com.wire.android.util.permission.PermissionDenialType +import com.wire.android.util.ui.SnackBarMessageHandler import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.user.UserId import kotlinx.coroutines.flow.MutableSharedFlow @@ -69,43 +71,74 @@ import kotlinx.coroutines.flow.MutableSharedFlow // Since the HomeScreen is responsible for displaying the bottom sheet content, // we create a bridge that passes the content of the BottomSheet // also we expose the lambda which expands the BottomSheet from the HomeScreen +@OptIn(ExperimentalMaterial3Api::class) @Suppress("ComplexMethod") @Composable fun ConversationRouterHomeBridge( navigator: Navigator, conversationItemType: ConversationItemType, - onHomeBottomSheetContentChanged: (@Composable ColumnScope.() -> Unit) -> Unit, - onOpenBottomSheet: () -> Unit, - onCloseBottomSheet: () -> Unit, - onSnackBarStateChanged: (HomeSnackbarState) -> Unit, searchBarState: SearchBarState, - isBottomSheetVisible: () -> Boolean, - conversationsSource: ConversationsSource = ConversationsSource.MAIN + conversationsSource: ConversationsSource = ConversationsSource.MAIN, + conversationListViewModel: ConversationListViewModel = hiltViewModel(), + conversationCallListViewModel: ConversationCallListViewModel = hiltViewModel(), ) { + var currentConversationItem by remember { + mutableStateOf(null) + } + var currentConversationOptionNavigation by remember { + mutableStateOf(ConversationOptionNavigation.Home) + } + + val sheetState = rememberModalBottomSheetState( + skipPartiallyExpanded = true, + confirmValueChange = { true }, + ) + val coroutineScope = rememberCoroutineScope() + val permissionPermanentlyDeniedDialogState = rememberVisibilityState() - val viewModel: ConversationListViewModel = hiltViewModel() - val activity = LocalActivity.current LaunchedEffect(conversationsSource) { - viewModel.updateConversationsSource(conversationsSource) + conversationListViewModel.updateConversationsSource(conversationsSource) + } + + LaunchedEffect(key1 = currentConversationItem) { + if (currentConversationItem != null) { + sheetState.show() + } else { + sheetState.hide() + } + } + + LaunchedEffect(sheetState.currentValue) { + if (sheetState.currentValue == SheetValue.Hidden) { + currentConversationItem = null + } + } + + LaunchedEffect(Unit) { + conversationListViewModel.infoMessage.collect { + currentConversationItem = null + } + } + + LaunchedEffect(Unit) { + conversationListViewModel.closeBottomSheet.collect { + currentConversationItem = null + } } val conversationRouterHomeState = rememberConversationRouterState( initialConversationItemType = conversationItemType, - homeSnackBarState = viewModel.homeSnackBarState, - closeBottomSheetState = viewModel.closeBottomSheet, - requestInProgress = viewModel.requestInProgress, - onSnackBarStateChanged = onSnackBarStateChanged, - onCloseBottomSheet = onCloseBottomSheet, + requestInProgress = conversationListViewModel.requestInProgress ) with(searchBarState) { LaunchedEffect(isSearchActive) { if (isSearchActive) { - viewModel.refreshMissingMetadata() + conversationListViewModel.refreshMissingMetadata() conversationRouterHomeState.openSearch() } else { conversationRouterHomeState.closeSearch() @@ -113,14 +146,14 @@ fun ConversationRouterHomeBridge( } LaunchedEffect(searchQuery) { - viewModel.searchConversation(searchQuery) + conversationListViewModel.searchConversation(searchQuery) } } fun showConfirmationDialogOrUnarchive(): (DialogState) -> Unit { return { dialogState -> if (dialogState.isArchived) { - viewModel.moveConversationToArchive(dialogState) + conversationListViewModel.moveConversationToArchive(dialogState) } else { conversationRouterHomeState.archiveConversationDialogState.show(dialogState) } @@ -128,69 +161,10 @@ fun ConversationRouterHomeBridge( } with(conversationRouterHomeState) { - fun openConversationBottomSheet( - conversationItem: ConversationItem, - conversationOptionNavigation: ConversationOptionNavigation = ConversationOptionNavigation.Home - ) { - onHomeBottomSheetContentChanged { - // if we just use [conversationItem] we won't be able to observe changes in conversation details (e.g. name changing). - // So we need to find ConversationItem in the State by id and use it for BottomSheet content. - val item: ConversationItem? = viewModel.conversationListState.findConversationById(conversationItem.conversationId) - - val conversationState = rememberConversationSheetState( - conversationItem = item ?: conversationItem, - conversationOptionNavigation = conversationOptionNavigation - ) - // if we reopen the BottomSheet of the previous conversation for example: - // when the user swipes down the BottomSheet manually when having mute option open - // we want to reopen it in the "home" section, but ONLY when the user reopens the BottomSheet - // by holding the conversation item, not when the notification icon is pressed, therefore when - // conversationOptionNavigation is equal to ConversationOptionNavigation.MutingNotificationOption - conversationState.conversationId?.let { conversationId -> - if (conversationId == conversationItem.conversationId && - conversationOptionNavigation != ConversationOptionNavigation.MutingNotificationOption - ) { - conversationState.toHome() - } - } - - ConversationSheetContent( - conversationSheetState = conversationState, - onMutingConversationStatusChange = { - viewModel.muteConversation( - conversationId = conversationState.conversationId, - mutedConversationStatus = conversationState.conversationSheetContent!!.mutingConversationState - ) - }, - addConversationToFavourites = viewModel::addConversationToFavourites, - moveConversationToFolder = viewModel::moveConversationToFolder, - updateConversationArchiveStatus = showConfirmationDialogOrUnarchive(), - clearConversationContent = clearContentDialogState::show, - blockUser = blockUserDialogState::show, - unblockUser = unblockUserDialogState::show, - leaveGroup = leaveGroupDialogState::show, - deleteGroup = deleteGroupDialogState::show, - closeBottomSheet = onCloseBottomSheet, - isBottomSheetVisible = isBottomSheetVisible - ) - } - onOpenBottomSheet() - } - val onEditConversationItem: (ConversationItem) -> Unit = remember { - { conversationItem -> - openConversationBottomSheet( - conversationItem = conversationItem - ) - } - } - - val onEditNotifications: (ConversationItem) -> Unit = remember { - { conversationItem -> - openConversationBottomSheet( - conversationItem = conversationItem, - conversationOptionNavigation = ConversationOptionNavigation.MutingNotificationOption - ) + { + currentConversationItem = it + currentConversationOptionNavigation = ConversationOptionNavigation.Home } } @@ -208,7 +182,7 @@ fun ConversationRouterHomeBridge( } } - with(viewModel.conversationListState) { + with(conversationListViewModel.conversationListState) { when (conversationRouterHomeState.conversationItemType) { ConversationItemType.ALL_CONVERSATIONS -> AllConversationScreenContent( @@ -228,26 +202,11 @@ fun ConversationRouterHomeBridge( ) ) } - } - ) - - ConversationItemType.CALLS -> - CallsScreenContent( - missedCalls = missedCalls, - callHistory = callHistory, - onCallItemClick = onOpenConversation, - onEditConversationItem = onEditConversationItem, - onOpenUserProfile = onOpenUserProfile - ) - - ConversationItemType.MENTIONS -> - MentionScreenContent( - unreadMentions = unreadMentions, - allMentions = allMentions, - onMentionItemClick = onOpenConversation, - onEditConversationItem = onEditConversationItem, - onOpenUserProfile = onOpenUserProfile, - openConversationNotificationsSettings = onEditNotifications + }, + conversationListCallState = conversationCallListViewModel.conversationListCallState, + dismissJoinCallAnywayDialog = conversationCallListViewModel::dismissJoinCallAnywayDialog, + joinCallAnyway = conversationCallListViewModel::joinAnyway, + joinOngoingCall = conversationCallListViewModel::joinOngoingCall ) ConversationItemType.SEARCH -> { @@ -258,7 +217,9 @@ fun ConversationRouterHomeBridge( onOpenConversation = onOpenConversation, onEditConversation = onEditConversationItem, onOpenUserProfile = onOpenUserProfile, - onJoinCall = { viewModel.joinOngoingCall(it, onJoinedCall) }, + onJoinCall = { + conversationCallListViewModel.joinOngoingCall(it, onJoinedCall) + }, onPermissionPermanentlyDenied = { } ) } @@ -273,42 +234,81 @@ fun ConversationRouterHomeBridge( BlockUserDialogContent( isLoading = requestInProgress, dialogState = blockUserDialogState, - onBlock = viewModel::blockUser + onBlock = conversationListViewModel::blockUser ) DeleteConversationGroupDialog( isLoading = requestInProgress, dialogState = deleteGroupDialogState, - onDeleteGroup = viewModel::deleteGroup + onDeleteGroup = conversationListViewModel::deleteGroup ) LeaveConversationGroupDialog( dialogState = leaveGroupDialogState, isLoading = requestInProgress, - onLeaveGroup = viewModel::leaveGroup + onLeaveGroup = conversationListViewModel::leaveGroup ) UnblockUserDialogContent( dialogState = unblockUserDialogState, - onUnblock = viewModel::unblockUser, + onUnblock = conversationListViewModel::unblockUser, isLoading = requestInProgress, ) ClearConversationContentDialog( dialogState = clearContentDialogState, isLoading = requestInProgress, - onClearConversationContent = viewModel::clearConversationContent + onClearConversationContent = conversationListViewModel::clearConversationContent ) ArchiveConversationDialog( dialogState = archiveConversationDialogState, - onArchiveButtonClicked = viewModel::moveConversationToArchive + onArchiveButtonClicked = conversationListViewModel::moveConversationToArchive ) + currentConversationItem?.let { + WireModalSheetLayout2( + sheetState = sheetState, + coroutineScope = coroutineScope, + sheetContent = { + val conversationState = rememberConversationSheetState( + conversationItem = it, + conversationOptionNavigation = currentConversationOptionNavigation + ) + + ConversationSheetContent( + conversationSheetState = conversationState, + onMutingConversationStatusChange = { + conversationListViewModel.muteConversation( + conversationId = conversationState.conversationId, + mutedConversationStatus = conversationState.conversationSheetContent!!.mutingConversationState + ) + }, + addConversationToFavourites = conversationListViewModel::addConversationToFavourites, + moveConversationToFolder = conversationListViewModel::moveConversationToFolder, + updateConversationArchiveStatus = showConfirmationDialogOrUnarchive(), + clearConversationContent = clearContentDialogState::show, + blockUser = blockUserDialogState::show, + unblockUser = unblockUserDialogState::show, + leaveGroup = leaveGroupDialogState::show, + deleteGroup = deleteGroupDialogState::show, + closeBottomSheet = { + currentConversationItem = null + } + ) + }, + onCloseBottomSheet = { + currentConversationItem = null + } + ) + } + BackHandler(conversationItemType == ConversationItemType.SEARCH) { closeSearch() } } + + SnackBarMessageHandler(infoMessages = conversationListViewModel.infoMessage) } @Suppress("LongParameterList") @@ -339,10 +339,6 @@ class ConversationRouterState( @Composable fun rememberConversationRouterState( initialConversationItemType: ConversationItemType, - homeSnackBarState: MutableSharedFlow, - onSnackBarStateChanged: (HomeSnackbarState) -> Unit, - closeBottomSheetState: MutableSharedFlow, - onCloseBottomSheet: () -> Unit, requestInProgress: Boolean ): ConversationRouterState { @@ -353,14 +349,6 @@ fun rememberConversationRouterState( val clearContentDialogState = rememberVisibilityState() val archiveConversationDialogState = rememberVisibilityState() - LaunchedEffect(Unit) { - homeSnackBarState.collect { onSnackBarStateChanged(it) } - } - - LaunchedEffect(Unit) { - closeBottomSheetState.collect { onCloseBottomSheet() } - } - val conversationRouterState = remember(initialConversationItemType) { ConversationRouterState( initialConversationItemType, @@ -391,5 +379,5 @@ fun rememberConversationRouterState( } enum class ConversationItemType { - ALL_CONVERSATIONS, CALLS, MENTIONS, SEARCH; + ALL_CONVERSATIONS, SEARCH; } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/all/AllConversationScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/all/AllConversationScreen.kt index a2cf3dccfc4..f0de4e2658f 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/all/AllConversationScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/all/AllConversationScreen.kt @@ -34,8 +34,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.hilt.navigation.compose.hiltViewModel import com.ramcosta.composedestinations.annotation.Destination import com.wire.android.R import com.wire.android.appLogger @@ -45,14 +43,16 @@ import com.wire.android.ui.common.dimensions import com.wire.android.ui.home.HomeStateHolder import com.wire.android.ui.home.archive.ArchivedConversationsEmptyStateScreen import com.wire.android.ui.home.conversationslist.ConversationItemType -import com.wire.android.ui.home.conversationslist.ConversationListViewModel +import com.wire.android.ui.home.conversationslist.ConversationListCallState import com.wire.android.ui.home.conversationslist.ConversationRouterHomeBridge import com.wire.android.ui.home.conversationslist.common.ConversationList import com.wire.android.ui.home.conversationslist.model.ConversationFolder import com.wire.android.ui.home.conversationslist.model.ConversationItem +import com.wire.android.ui.theme.WireTheme import com.wire.android.ui.theme.wireColorScheme import com.wire.android.ui.theme.wireTypography import com.wire.android.util.permission.PermissionDenialType +import com.wire.android.util.ui.PreviewMultipleThemes import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.user.UserId import kotlinx.collections.immutable.ImmutableMap @@ -66,12 +66,7 @@ fun AllConversationScreen(homeStateHolder: HomeStateHolder) { ConversationRouterHomeBridge( navigator = navigator, conversationItemType = ConversationItemType.ALL_CONVERSATIONS, - onHomeBottomSheetContentChanged = ::changeBottomSheetContent, - onOpenBottomSheet = ::openBottomSheet, - onCloseBottomSheet = ::closeBottomSheet, - onSnackBarStateChanged = ::setSnackBarState, searchBarState = searchBarState, - isBottomSheetVisible = ::isBottomSheetVisible ) } } @@ -79,23 +74,26 @@ fun AllConversationScreen(homeStateHolder: HomeStateHolder) { @Composable fun AllConversationScreenContent( conversations: ImmutableMap>, + conversationListCallState: ConversationListCallState, hasNoConversations: Boolean, - isFromArchive: Boolean = false, - viewModel: ConversationListViewModel = hiltViewModel(), onEditConversation: (ConversationItem) -> Unit, onOpenConversation: (ConversationId) -> Unit, onOpenUserProfile: (UserId) -> Unit, onJoinedCall: (ConversationId) -> Unit, - onPermissionPermanentlyDenied: (type: PermissionDenialType) -> Unit + onPermissionPermanentlyDenied: (type: PermissionDenialType) -> Unit, + dismissJoinCallAnywayDialog: () -> Unit, + joinCallAnyway: (conversationId: ConversationId, onJoinedCall: (ConversationId) -> Unit) -> Unit, + isFromArchive: Boolean = false, + joinOngoingCall: (conversationId: ConversationId, onJoinedCall: (ConversationId) -> Unit) -> Unit ) { val lazyListState = rememberLazyListState() val callConversationIdToJoin = remember { mutableStateOf(ConversationId("", "")) } - if (viewModel.conversationListCallState.shouldShowJoinAnywayDialog) { + if (conversationListCallState.shouldShowJoinAnywayDialog) { appLogger.i("$TAG showing showJoinAnywayDialog..") JoinAnywayDialog( - onDismiss = viewModel::dismissJoinCallAnywayDialog, - onConfirm = { viewModel.joinAnyway(callConversationIdToJoin.value, onJoinedCall) } + onDismiss = dismissJoinCallAnywayDialog, + onConfirm = { joinCallAnyway(callConversationIdToJoin.value, onJoinedCall) } ) } if (hasNoConversations) { @@ -114,7 +112,7 @@ fun AllConversationScreenContent( onOpenUserProfile = onOpenUserProfile, onJoinCall = { callConversationIdToJoin.value = it - viewModel.joinOngoingCall(it, onJoinedCall) + joinOngoingCall(it, onJoinedCall) }, onPermissionPermanentlyDenied = onPermissionPermanentlyDenied ) @@ -158,9 +156,9 @@ fun ConversationListEmptyStateScreen() { } } -@Preview +@PreviewMultipleThemes @Composable -fun PreviewAllConversationScreen() { +fun PreviewAllConversationScreen() = WireTheme { AllConversationScreenContent( conversations = persistentMapOf(), hasNoConversations = false, @@ -168,13 +166,18 @@ fun PreviewAllConversationScreen() { onOpenConversation = {}, onOpenUserProfile = {}, onJoinedCall = {}, - onPermissionPermanentlyDenied = {} + onPermissionPermanentlyDenied = {}, + conversationListCallState = ConversationListCallState(), + isFromArchive = false, + dismissJoinCallAnywayDialog = {}, + joinCallAnyway = { _, _ -> }, + joinOngoingCall = { _, _ -> } ) } -@Preview +@PreviewMultipleThemes @Composable -fun ConversationListEmptyStateScreenPreview() { +fun ConversationListEmptyStateScreenPreview() = WireTheme { AllConversationScreenContent( conversations = persistentMapOf(), hasNoConversations = true, @@ -182,7 +185,12 @@ fun ConversationListEmptyStateScreenPreview() { onOpenConversation = {}, onOpenUserProfile = {}, onJoinedCall = {}, - onPermissionPermanentlyDenied = {} + onPermissionPermanentlyDenied = {}, + conversationListCallState = ConversationListCallState(), + isFromArchive = false, + dismissJoinCallAnywayDialog = {}, + joinCallAnyway = { _, _ -> }, + joinOngoingCall = { _, _ -> } ) } 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 f67bce46fd5..aebd33a4d85 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 @@ -1,2 +1,39 @@ +/* + * 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.util.ui +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.platform.LocalContext +import com.wire.android.model.SnackBarMessage +import com.wire.android.ui.common.snackbar.LocalSnackbarHostState +import kotlinx.coroutines.flow.SharedFlow + +@Composable +fun SnackBarMessageHandler(infoMessages: SharedFlow) { + val context = LocalContext.current + val snackbarHostState = LocalSnackbarHostState.current + + LaunchedEffect(Unit) { + infoMessages.collect { + snackbarHostState.showSnackbar( + message = it.uiText.asString(context.resources) + ) + } + } +} diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversationslist/ConversationCallListViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversationslist/ConversationCallListViewModelTest.kt index 7bb992fcfba..5213a7360c4 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/conversationslist/ConversationCallListViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversationslist/ConversationCallListViewModelTest.kt @@ -19,49 +19,14 @@ package com.wire.android.ui.home.conversationslist -import androidx.compose.ui.text.input.TextFieldValue -import app.cash.turbine.test import com.wire.android.config.CoroutineTestExtension -import com.wire.android.config.TestDispatcherProvider import com.wire.android.config.mockUri import com.wire.android.framework.TestConversationDetails -import com.wire.android.mapper.UserTypeMapper -import com.wire.android.model.UserAvatarData -import com.wire.android.ui.common.bottomsheet.conversation.ConversationTypeDetail -import com.wire.android.ui.common.dialogs.BlockUserDialogState -import com.wire.android.ui.home.HomeSnackBarMessage -import com.wire.android.ui.home.conversations.model.UILastMessageContent -import com.wire.android.ui.home.conversationslist.model.BadgeEventType -import com.wire.android.ui.home.conversationslist.model.BlockingState -import com.wire.android.ui.home.conversationslist.model.ConversationFolder -import com.wire.android.ui.home.conversationslist.model.ConversationInfo -import com.wire.android.ui.home.conversationslist.model.ConversationItem -import com.wire.android.ui.home.conversationslist.model.ConversationsSource -import com.wire.android.ui.home.conversationslist.model.DialogState -import com.wire.android.ui.home.conversationslist.model.Membership -import com.wire.android.util.orDefault -import com.wire.android.util.ui.WireSessionImageLoader -import com.wire.kalium.logic.data.conversation.Conversation -import com.wire.kalium.logic.data.conversation.MutedConversationStatus import com.wire.kalium.logic.data.id.ConversationId -import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.feature.call.usecase.AnswerCallUseCase import com.wire.kalium.logic.feature.call.usecase.EndCallUseCase import com.wire.kalium.logic.feature.call.usecase.ObserveEstablishedCallsUseCase -import com.wire.kalium.logic.feature.connection.BlockUserResult -import com.wire.kalium.logic.feature.connection.BlockUserUseCase -import com.wire.kalium.logic.feature.connection.UnblockUserResult -import com.wire.kalium.logic.feature.connection.UnblockUserUseCase -import com.wire.kalium.logic.feature.conversation.ArchiveStatusUpdateResult -import com.wire.kalium.logic.feature.conversation.ClearConversationContentUseCase -import com.wire.kalium.logic.feature.conversation.ConversationUpdateStatusResult -import com.wire.kalium.logic.feature.conversation.LeaveConversationUseCase import com.wire.kalium.logic.feature.conversation.ObserveConversationListDetailsUseCase -import com.wire.kalium.logic.feature.conversation.RefreshConversationsWithoutMetadataUseCase -import com.wire.kalium.logic.feature.conversation.UpdateConversationArchivedStatusUseCase -import com.wire.kalium.logic.feature.conversation.UpdateConversationMutedStatusUseCase -import com.wire.kalium.logic.feature.publicuser.RefreshUsersWithoutMetadataUseCase -import com.wire.kalium.logic.feature.team.DeleteTeamConversationUseCase import io.mockk.MockKAnnotations import io.mockk.coEvery import io.mockk.coVerify @@ -75,59 +40,27 @@ import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.setMain import org.amshove.kluent.internal.assertEquals -import org.amshove.kluent.shouldBeEqualTo import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith @OptIn(ExperimentalCoroutinesApi::class) @ExtendWith(CoroutineTestExtension::class) -// TODO write more tests -class ConversationListViewModelTest { +class ConversationCallListViewModelTest { - private var conversationListViewModel: ConversationListViewModel - - @MockK - lateinit var updateConversationMutedStatus: UpdateConversationMutedStatusUseCase + private var conversationCallListViewModel: ConversationCallListViewModel @MockK lateinit var observeConversationListDetailsUseCase: ObserveConversationListDetailsUseCase - @MockK - lateinit var leaveConversation: LeaveConversationUseCase - - @MockK - lateinit var deleteTeamConversationUseCase: DeleteTeamConversationUseCase - @MockK lateinit var joinCall: AnswerCallUseCase - @MockK - lateinit var blockUser: BlockUserUseCase - - @MockK - lateinit var unblockUser: UnblockUserUseCase - - @MockK - lateinit var clearConversationContent: ClearConversationContentUseCase - - @MockK - private lateinit var wireSessionImageLoader: WireSessionImageLoader - @MockK private lateinit var endCall: EndCallUseCase @MockK private lateinit var observeEstablishedCalls: ObserveEstablishedCallsUseCase - @MockK - private lateinit var refreshUsersWithoutMetadata: RefreshUsersWithoutMetadataUseCase - - @MockK - private lateinit var refreshConversationsWithoutMetadata: RefreshConversationsWithoutMetadataUseCase - - @MockK - private lateinit var updateConversationArchivedStatus: UpdateConversationArchivedStatusUseCase - @MockK(relaxed = true) private lateinit var onJoined: (ConversationId) -> Unit @@ -147,248 +80,70 @@ class ConversationListViewModelTest { ) mockUri() - conversationListViewModel = - ConversationListViewModel( - dispatcher = TestDispatcherProvider(), - updateConversationMutedStatus = updateConversationMutedStatus, + conversationCallListViewModel = + ConversationCallListViewModel( answerCall = joinCall, - observeConversationListDetails = observeConversationListDetailsUseCase, - leaveConversation = leaveConversation, - deleteTeamConversation = deleteTeamConversationUseCase, - blockUserUseCase = blockUser, - unblockUserUseCase = unblockUser, - clearConversationContentUseCase = clearConversationContent, - wireSessionImageLoader = wireSessionImageLoader, endCall = endCall, - observeEstablishedCalls = observeEstablishedCalls, - refreshUsersWithoutMetadata = refreshUsersWithoutMetadata, - refreshConversationsWithoutMetadata = refreshConversationsWithoutMetadata, - userTypeMapper = UserTypeMapper(), - updateConversationArchivedStatus = updateConversationArchivedStatus + observeEstablishedCalls = observeEstablishedCalls ) } - @Test - fun `given empty search query, when collecting, then update state with all conversations`() = runTest { - // Given - val searchQueryText = "" - - // When - dispatcher.scheduler.advanceUntilIdle() - conversationListViewModel.searchConversation(TextFieldValue(searchQueryText)) - dispatcher.scheduler.advanceUntilIdle() - - // Then - assertEquals( - 3, - conversationListViewModel.conversationListState.conversationSearchResult[ConversationFolder.Predefined.Conversations]?.size, - ) - assertEquals(searchQueryText, conversationListViewModel.conversationListState.searchQuery) - } - - @Test - fun `given non-empty search query, when collecting, then update state with filtered conversations`() = runTest { - // Given - val searchQueryText = TestConversationDetails.CONVERSATION_ONE_ONE.conversation.name.orDefault("test") - - // When - dispatcher.scheduler.advanceUntilIdle() - conversationListViewModel.searchConversation(TextFieldValue(searchQueryText)) - dispatcher.scheduler.advanceUntilIdle() - - // Then - assertEquals( - 1, - conversationListViewModel.conversationListState.conversationSearchResult[ConversationFolder.Predefined.Conversations]?.size, - ) - assertEquals(searchQueryText, conversationListViewModel.conversationListState.searchQuery) - } - - @Test - fun `given empty search query, when collecting archived conversations, then update state with only archived conversations`() = runTest { - // Given - coEvery { observeConversationListDetailsUseCase.invoke(true) } returns flowOf( - listOf( - TestConversationDetails.CONVERSATION_ONE_ONE, - TestConversationDetails.GROUP - ) - ) - // When - dispatcher.scheduler.advanceUntilIdle() - conversationListViewModel.updateConversationsSource(ConversationsSource.ARCHIVE) - dispatcher.scheduler.advanceUntilIdle() - - // Then - assertEquals( - 2, - conversationListViewModel.conversationListState.conversationSearchResult[ConversationFolder.WithoutHeader]?.size, - ) - coVerify(exactly = 1) { observeConversationListDetailsUseCase.invoke(true) } - } - - @Test - fun `given a valid conversation muting state, when calling muteConversation, then should call with call the UseCase`() = runTest { - coEvery { updateConversationMutedStatus(any(), any(), any()) } returns ConversationUpdateStatusResult.Success - conversationListViewModel.muteConversation(conversationId, MutedConversationStatus.AllMuted) - - coVerify(exactly = 1) { updateConversationMutedStatus(conversationId, MutedConversationStatus.AllMuted, any()) } - } - @Test fun `given a conversation id, when joining an ongoing call, then verify that answer call usecase is called`() = runTest { coEvery { joinCall(any()) } returns Unit - conversationListViewModel.joinOngoingCall(conversationId = conversationId, onJoined = onJoined) + conversationCallListViewModel.joinOngoingCall(conversationId = conversationId, onJoined = onJoined) coVerify(exactly = 1) { joinCall(conversationId = conversationId) } verify(exactly = 1) { onJoined(conversationId) } } - @Test - fun `given a valid conversation muting state, when calling block user, then should call BlockUserUseCase`() = runTest { - coEvery { blockUser(any()) } returns BlockUserResult.Success - conversationListViewModel.blockUser( - BlockUserDialogState( - userName = "someName", - userId = userId - ) - ) - - coVerify(exactly = 1) { blockUser(userId) } - } - - @Test - fun `given a valid conversation muting state, when calling unblock user, then should call BlockUserUseCase`() = runTest { - coEvery { unblockUser(any()) } returns UnblockUserResult.Success - conversationListViewModel.unblockUser(userId) - - coVerify(exactly = 1) { unblockUser(userId) } - } - @Test fun `given join dialog displayed, when user dismiss it, then hide it`() { - conversationListViewModel.conversationListCallState = conversationListViewModel.conversationListCallState.copy( + conversationCallListViewModel.conversationListCallState = conversationCallListViewModel.conversationListCallState.copy( shouldShowJoinAnywayDialog = true ) - conversationListViewModel.dismissJoinCallAnywayDialog() + conversationCallListViewModel.dismissJoinCallAnywayDialog() - assertEquals(false, conversationListViewModel.conversationListCallState.shouldShowJoinAnywayDialog) + assertEquals(false, conversationCallListViewModel.conversationListCallState.shouldShowJoinAnywayDialog) } @Test fun `given no ongoing call, when user tries to join a call, then invoke answerCall call use case`() { - conversationListViewModel.conversationListCallState = conversationListViewModel.conversationListCallState.copy(hasEstablishedCall = false) + conversationCallListViewModel.conversationListCallState = conversationCallListViewModel.conversationListCallState.copy(hasEstablishedCall = false) coEvery { joinCall(conversationId = any()) } returns Unit - conversationListViewModel.joinOngoingCall(conversationId, onJoined) + conversationCallListViewModel.joinOngoingCall(conversationId, onJoined) coVerify(exactly = 1) { joinCall(conversationId = any()) } coVerify(exactly = 1) { onJoined(any()) } - assertEquals(false, conversationListViewModel.conversationListCallState.shouldShowJoinAnywayDialog) + assertEquals(false, conversationCallListViewModel.conversationListCallState.shouldShowJoinAnywayDialog) } @Test fun `given an ongoing call, when user tries to join a call, then show JoinCallAnywayDialog`() { - conversationListViewModel.conversationListCallState = conversationListViewModel.conversationListCallState.copy(hasEstablishedCall = true) + conversationCallListViewModel.conversationListCallState = conversationCallListViewModel.conversationListCallState.copy(hasEstablishedCall = true) - conversationListViewModel.joinOngoingCall(conversationId, onJoined) + conversationCallListViewModel.joinOngoingCall(conversationId, onJoined) - assertEquals(true, conversationListViewModel.conversationListCallState.shouldShowJoinAnywayDialog) + assertEquals(true, conversationCallListViewModel.conversationListCallState.shouldShowJoinAnywayDialog) coVerify(inverse = true) { joinCall(conversationId = any()) } } @Test fun `given an ongoing call, when user confirms dialog to join a call, then end current call and join the newer one`() { - conversationListViewModel.conversationListCallState = conversationListViewModel.conversationListCallState.copy(hasEstablishedCall = true) - conversationListViewModel.establishedCallConversationId = ConversationId("value", "Domain") + conversationCallListViewModel.conversationListCallState = conversationCallListViewModel.conversationListCallState.copy(hasEstablishedCall = true) + conversationCallListViewModel.establishedCallConversationId = ConversationId("value", "Domain") coEvery { endCall(any()) } returns Unit - conversationListViewModel.joinAnyway(conversationId, onJoined) + conversationCallListViewModel.joinAnyway(conversationId, onJoined) coVerify(exactly = 1) { endCall(any()) } } - @Test - fun `given a valid conversation state, when archiving it correctly, then the right success message is shown`() = runTest { - val isArchiving = true - val dialogState = DialogState( - conversationItem.conversationId, - conversationItem.conversationInfo.name, - ConversationTypeDetail.Private(null, conversationItem.userId, BlockingState.NOT_BLOCKED), - !isArchiving, - true - ) - val archivingTimestamp = 123456789L - - coEvery { updateConversationArchivedStatus(any(), any(), any(), any()) } returns ArchiveStatusUpdateResult.Success - - conversationListViewModel.homeSnackBarState.test { - conversationListViewModel.moveConversationToArchive(dialogState, archivingTimestamp) - expectMostRecentItem() shouldBeEqualTo HomeSnackBarMessage.UpdateArchivingStatusSuccess(isArchiving = isArchiving) - } - coVerify(exactly = 1) { - updateConversationArchivedStatus.invoke( - dialogState.conversationId, - !dialogState.isArchived, - onlyLocally = false, - archivingTimestamp - ) - } - } - - @Test - fun `given a valid conversation state, when un-archiving it with an error, then the right failure message is shown`() = runTest { - val isArchiving = false - val dialogState = DialogState( - conversationItem.conversationId, - conversationItem.conversationInfo.name, - ConversationTypeDetail.Private(null, conversationItem.userId, BlockingState.NOT_BLOCKED), - !isArchiving, - isMember = true - ) - val archivingTimestamp = 123456789L - - coEvery { updateConversationArchivedStatus(any(), any(), any(), any()) } returns ArchiveStatusUpdateResult.Failure - - conversationListViewModel.homeSnackBarState.test { - conversationListViewModel.moveConversationToArchive(dialogState, archivingTimestamp) - expectMostRecentItem() shouldBeEqualTo HomeSnackBarMessage.UpdateArchivingStatusError(isArchiving = isArchiving) - } - coVerify(exactly = 1) { - updateConversationArchivedStatus.invoke( - dialogState.conversationId, - !dialogState.isArchived, - false, - archivingTimestamp, - ) - } - } - companion object { private val conversationId = ConversationId("some_id", "some_domain") - private val userId: UserId = UserId("someUser", "some_domain") - - private val testConversations = TestConversationDetails.CONVERSATION_ONE_ONE - - private val conversationItem = ConversationItem.PrivateConversation( - userAvatarData = UserAvatarData(), - conversationInfo = ConversationInfo( - name = "Some dummy name", - membership = Membership.None - ), - conversationId = conversationId, - mutedStatus = MutedConversationStatus.AllAllowed, - isLegalHold = false, - lastMessageContent = UILastMessageContent.None, - badgeEventType = BadgeEventType.None, - userId = userId, - blockingState = BlockingState.CAN_NOT_BE_BLOCKED, - teamId = null, - isArchived = false, - mlsVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED, - proteusVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED - ) } } diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModelTest.kt index a0977037769..60764441b08 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModelTest.kt @@ -20,39 +20,24 @@ package com.wire.android.ui.home.conversationslist import androidx.compose.ui.text.input.TextFieldValue -import app.cash.turbine.test import com.wire.android.config.CoroutineTestExtension import com.wire.android.config.TestDispatcherProvider import com.wire.android.config.mockUri import com.wire.android.framework.TestConversationDetails import com.wire.android.mapper.UserTypeMapper -import com.wire.android.model.UserAvatarData -import com.wire.android.ui.common.bottomsheet.conversation.ConversationTypeDetail import com.wire.android.ui.common.dialogs.BlockUserDialogState -import com.wire.android.ui.home.HomeSnackbarState -import com.wire.android.ui.home.conversations.model.UILastMessageContent -import com.wire.android.ui.home.conversationslist.model.BadgeEventType -import com.wire.android.ui.home.conversationslist.model.BlockingState import com.wire.android.ui.home.conversationslist.model.ConversationFolder -import com.wire.android.ui.home.conversationslist.model.ConversationInfo -import com.wire.android.ui.home.conversationslist.model.ConversationItem import com.wire.android.ui.home.conversationslist.model.ConversationsSource -import com.wire.android.ui.home.conversationslist.model.DialogState -import com.wire.android.ui.home.conversationslist.model.Membership import com.wire.android.util.orDefault import com.wire.android.util.ui.WireSessionImageLoader -import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.conversation.MutedConversationStatus import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.user.UserId -import com.wire.kalium.logic.feature.call.usecase.AnswerCallUseCase -import com.wire.kalium.logic.feature.call.usecase.EndCallUseCase import com.wire.kalium.logic.feature.call.usecase.ObserveEstablishedCallsUseCase import com.wire.kalium.logic.feature.connection.BlockUserResult import com.wire.kalium.logic.feature.connection.BlockUserUseCase import com.wire.kalium.logic.feature.connection.UnblockUserResult import com.wire.kalium.logic.feature.connection.UnblockUserUseCase -import com.wire.kalium.logic.feature.conversation.ArchiveStatusUpdateResult import com.wire.kalium.logic.feature.conversation.ClearConversationContentUseCase import com.wire.kalium.logic.feature.conversation.ConversationUpdateStatusResult import com.wire.kalium.logic.feature.conversation.LeaveConversationUseCase @@ -66,7 +51,6 @@ import io.mockk.MockKAnnotations import io.mockk.coEvery import io.mockk.coVerify import io.mockk.impl.annotations.MockK -import io.mockk.verify import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.emptyFlow @@ -75,7 +59,6 @@ import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.setMain import org.amshove.kluent.internal.assertEquals -import org.amshove.kluent.shouldBeEqualTo import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith @@ -98,9 +81,6 @@ class ConversationListViewModelTest { @MockK lateinit var deleteTeamConversationUseCase: DeleteTeamConversationUseCase - @MockK - lateinit var joinCall: AnswerCallUseCase - @MockK lateinit var blockUser: BlockUserUseCase @@ -113,9 +93,6 @@ class ConversationListViewModelTest { @MockK private lateinit var wireSessionImageLoader: WireSessionImageLoader - @MockK - private lateinit var endCall: EndCallUseCase - @MockK private lateinit var observeEstablishedCalls: ObserveEstablishedCallsUseCase @@ -128,9 +105,6 @@ class ConversationListViewModelTest { @MockK private lateinit var updateConversationArchivedStatus: UpdateConversationArchivedStatusUseCase - @MockK(relaxed = true) - private lateinit var onJoined: (ConversationId) -> Unit - private val dispatcher = StandardTestDispatcher() init { @@ -151,7 +125,6 @@ class ConversationListViewModelTest { ConversationListViewModel( dispatcher = TestDispatcherProvider(), updateConversationMutedStatus = updateConversationMutedStatus, - answerCall = joinCall, observeConversationListDetails = observeConversationListDetailsUseCase, leaveConversation = leaveConversation, deleteTeamConversation = deleteTeamConversationUseCase, @@ -159,7 +132,6 @@ class ConversationListViewModelTest { unblockUserUseCase = unblockUser, clearConversationContentUseCase = clearConversationContent, wireSessionImageLoader = wireSessionImageLoader, - endCall = endCall, observeEstablishedCalls = observeEstablishedCalls, refreshUsersWithoutMetadata = refreshUsersWithoutMetadata, refreshConversationsWithoutMetadata = refreshConversationsWithoutMetadata, @@ -234,16 +206,6 @@ class ConversationListViewModelTest { coVerify(exactly = 1) { updateConversationMutedStatus(conversationId, MutedConversationStatus.AllMuted, any()) } } - @Test - fun `given a conversation id, when joining an ongoing call, then verify that answer call usecase is called`() = runTest { - coEvery { joinCall(any()) } returns Unit - - conversationListViewModel.joinOngoingCall(conversationId = conversationId, onJoined = onJoined) - - coVerify(exactly = 1) { joinCall(conversationId = conversationId) } - verify(exactly = 1) { onJoined(conversationId) } - } - @Test fun `given a valid conversation muting state, when calling block user, then should call BlockUserUseCase`() = runTest { coEvery { blockUser(any()) } returns BlockUserResult.Success @@ -265,130 +227,8 @@ class ConversationListViewModelTest { coVerify(exactly = 1) { unblockUser(userId) } } - @Test - fun `given join dialog displayed, when user dismiss it, then hide it`() { - conversationListViewModel.conversationListCallState = conversationListViewModel.conversationListCallState.copy( - shouldShowJoinAnywayDialog = true - ) - - conversationListViewModel.dismissJoinCallAnywayDialog() - - assertEquals(false, conversationListViewModel.conversationListCallState.shouldShowJoinAnywayDialog) - } - - @Test - fun `given no ongoing call, when user tries to join a call, then invoke answerCall call use case`() { - conversationListViewModel.conversationListCallState = conversationListViewModel.conversationListCallState.copy(hasEstablishedCall = false) - - coEvery { joinCall(conversationId = any()) } returns Unit - - conversationListViewModel.joinOngoingCall(conversationId, onJoined) - - coVerify(exactly = 1) { joinCall(conversationId = any()) } - coVerify(exactly = 1) { onJoined(any()) } - assertEquals(false, conversationListViewModel.conversationListCallState.shouldShowJoinAnywayDialog) - } - - @Test - fun `given an ongoing call, when user tries to join a call, then show JoinCallAnywayDialog`() { - conversationListViewModel.conversationListCallState = conversationListViewModel.conversationListCallState.copy(hasEstablishedCall = true) - - conversationListViewModel.joinOngoingCall(conversationId, onJoined) - - assertEquals(true, conversationListViewModel.conversationListCallState.shouldShowJoinAnywayDialog) - coVerify(inverse = true) { joinCall(conversationId = any()) } - } - - @Test - fun `given an ongoing call, when user confirms dialog to join a call, then end current call and join the newer one`() { - conversationListViewModel.conversationListCallState = conversationListViewModel.conversationListCallState.copy(hasEstablishedCall = true) - conversationListViewModel.establishedCallConversationId = ConversationId("value", "Domain") - coEvery { endCall(any()) } returns Unit - - conversationListViewModel.joinAnyway(conversationId, onJoined) - - coVerify(exactly = 1) { endCall(any()) } - } - - @Test - fun `given a valid conversation state, when archiving it correctly, then the right success message is shown`() = runTest { - val isArchiving = true - val dialogState = DialogState( - conversationItem.conversationId, - conversationItem.conversationInfo.name, - ConversationTypeDetail.Private(null, conversationItem.userId, BlockingState.NOT_BLOCKED), - !isArchiving, - true - ) - val archivingTimestamp = 123456789L - - coEvery { updateConversationArchivedStatus(any(), any(), any(), any()) } returns ArchiveStatusUpdateResult.Success - - conversationListViewModel.homeSnackBarState.test { - conversationListViewModel.moveConversationToArchive(dialogState, archivingTimestamp) - expectMostRecentItem() shouldBeEqualTo HomeSnackbarState.UpdateArchivingStatusSuccess(isArchiving = isArchiving) - } - coVerify(exactly = 1) { - updateConversationArchivedStatus.invoke( - dialogState.conversationId, - !dialogState.isArchived, - onlyLocally = false, - archivingTimestamp - ) - } - } - - @Test - fun `given a valid conversation state, when un-archiving it with an error, then the right failure message is shown`() = runTest { - val isArchiving = false - val dialogState = DialogState( - conversationItem.conversationId, - conversationItem.conversationInfo.name, - ConversationTypeDetail.Private(null, conversationItem.userId, BlockingState.NOT_BLOCKED), - !isArchiving, - isMember = true - ) - val archivingTimestamp = 123456789L - - coEvery { updateConversationArchivedStatus(any(), any(), any(), any()) } returns ArchiveStatusUpdateResult.Failure - - conversationListViewModel.homeSnackBarState.test { - conversationListViewModel.moveConversationToArchive(dialogState, archivingTimestamp) - expectMostRecentItem() shouldBeEqualTo HomeSnackbarState.UpdateArchivingStatusError(isArchiving = isArchiving) - } - coVerify(exactly = 1) { - updateConversationArchivedStatus.invoke( - dialogState.conversationId, - !dialogState.isArchived, - false, - archivingTimestamp, - ) - } - } - companion object { private val conversationId = ConversationId("some_id", "some_domain") private val userId: UserId = UserId("someUser", "some_domain") - - private val testConversations = TestConversationDetails.CONVERSATION_ONE_ONE - - private val conversationItem = ConversationItem.PrivateConversation( - userAvatarData = UserAvatarData(), - conversationInfo = ConversationInfo( - name = "Some dummy name", - membership = Membership.None - ), - conversationId = conversationId, - mutedStatus = MutedConversationStatus.AllAllowed, - isLegalHold = false, - lastMessageContent = UILastMessageContent.None, - badgeEventType = BadgeEventType.None, - userId = userId, - blockingState = BlockingState.CAN_NOT_BE_BLOCKED, - teamId = null, - isArchived = false, - mlsVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED, - proteusVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED - ) } } diff --git a/core/ui-common/src/main/kotlin/com/wire/android/ui/common/bottomsheet/WireModalSheetLayout.kt b/core/ui-common/src/main/kotlin/com/wire/android/ui/common/bottomsheet/WireModalSheetLayout.kt index e8de411621f..465acc6233b 100644 --- a/core/ui-common/src/main/kotlin/com/wire/android/ui/common/bottomsheet/WireModalSheetLayout.kt +++ b/core/ui-common/src/main/kotlin/com/wire/android/ui/common/bottomsheet/WireModalSheetLayout.kt @@ -25,6 +25,7 @@ import androidx.compose.foundation.layout.absoluteOffset import androidx.compose.material3.BottomSheetDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.SheetState import androidx.compose.material3.SheetValue import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -93,3 +94,44 @@ fun MenuModalSheetContent( buildMenuSheetItems(items = menuItems) } } + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun WireModalSheetLayout2( + sheetState: SheetState, + coroutineScope: CoroutineScope, + sheetContent: @Composable ColumnScope.() -> Unit, + modifier: Modifier = Modifier, + sheetShape: Shape = WireBottomSheetDefaults.WireBottomSheetShape, + containerColor: Color = WireBottomSheetDefaults.WireSheetContainerColor, + contentColor: Color = WireBottomSheetDefaults.WireSheetContentColor, + tonalElevation: Dp = WireBottomSheetDefaults.WireSheetTonalElevation, + scrimColor: Color = BottomSheetDefaults.ScrimColor, + dragHandle: @Composable (() -> Unit)? = { WireBottomSheetDefaults.WireDragHandle() }, + onCloseBottomSheet: () -> Unit +) { + ModalBottomSheet( + sheetState = sheetState, + shape = sheetShape, + content = sheetContent, + containerColor = containerColor, + contentColor = contentColor, + scrimColor = scrimColor, + tonalElevation = tonalElevation, + onDismissRequest = { + coroutineScope.launch { + sheetState.hide() + } + }, + dragHandle = dragHandle, + modifier = modifier.absoluteOffset(y = 1.dp) + ) + + BackHandler(enabled = sheetState.isVisible) { + coroutineScope.launch { sheetState.hide() }.invokeOnCompletion { + if (!sheetState.isVisible) { + onCloseBottomSheet() + } + } + } +}