diff --git a/app/src/main/kotlin/com/wire/android/di/accountScoped/MessageModule.kt b/app/src/main/kotlin/com/wire/android/di/accountScoped/MessageModule.kt index ad8ce4242be..e20b411961e 100644 --- a/app/src/main/kotlin/com/wire/android/di/accountScoped/MessageModule.kt +++ b/app/src/main/kotlin/com/wire/android/di/accountScoped/MessageModule.kt @@ -28,6 +28,7 @@ import com.wire.kalium.logic.feature.asset.GetMessageAssetUseCase import com.wire.kalium.logic.feature.asset.ScheduleNewAssetMessageUseCase import com.wire.kalium.logic.feature.asset.UpdateAssetMessageDownloadStatusUseCase import com.wire.kalium.logic.feature.message.DeleteMessageUseCase +import com.wire.kalium.logic.feature.message.GetConversationMessagesFromSearchQueryUseCase import com.wire.kalium.logic.feature.message.GetMessageByIdUseCase import com.wire.kalium.logic.feature.message.GetNotificationsUseCase import com.wire.kalium.logic.feature.message.GetPaginatedFlowOfMessagesByConversationUseCase @@ -147,4 +148,9 @@ class MessageModule { @Provides fun provideGetPaginatedMessagesUseCase(messageScope: MessageScope): GetPaginatedFlowOfMessagesByConversationUseCase = messageScope.getPaginatedFlowOfMessagesByConversation + + @ViewModelScoped + @Provides + fun provideGetConversationMessagesFromSearchQueryUseCase(messageScope: MessageScope): GetConversationMessagesFromSearchQueryUseCase = + messageScope.getConversationMessagesFromSearchQuery } diff --git a/app/src/main/kotlin/com/wire/android/ui/common/SearchBar.kt b/app/src/main/kotlin/com/wire/android/ui/common/SearchBar.kt index 4e3db7fa6ad..b05ebb9c8c9 100644 --- a/app/src/main/kotlin/com/wire/android/ui/common/SearchBar.kt +++ b/app/src/main/kotlin/com/wire/android/ui/common/SearchBar.kt @@ -31,11 +31,13 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle @@ -73,6 +75,7 @@ fun SearchBar( ) } +@OptIn(ExperimentalComposeUiApi::class) @Composable fun SearchBarInput( placeholderText: String, @@ -82,10 +85,15 @@ fun SearchBarInput( placeholderTextStyle: TextStyle = LocalTextStyle.current, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, textStyle: TextStyle = LocalTextStyle.current, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, + shouldRequestFocus: Boolean = false ) { + val focusRequester = remember { FocusRequester() } + val keyboardController = LocalSoftwareKeyboardController.current + WireTextField( - modifier = modifier, + modifier = modifier + .focusRequester(focusRequester), value = text, onValueChange = onTextTyped, leadingIcon = { @@ -116,6 +124,13 @@ fun SearchBarInput( maxLines = 1, singleLine = true, ) + + if (shouldRequestFocus) { + LaunchedEffect(Unit) { + focusRequester.requestFocus() + keyboardController?.show() + } + } } @Preview(showBackground = true) diff --git a/app/src/main/kotlin/com/wire/android/ui/common/topappbar/search/SearchTopBar.kt b/app/src/main/kotlin/com/wire/android/ui/common/topappbar/search/SearchTopBar.kt index 6c72e05c290..e3c60f07449 100644 --- a/app/src/main/kotlin/com/wire/android/ui/common/topappbar/search/SearchTopBar.kt +++ b/app/src/main/kotlin/com/wire/android/ui/common/topappbar/search/SearchTopBar.kt @@ -59,16 +59,18 @@ import com.wire.android.ui.theme.wireDimensions @OptIn(ExperimentalAnimationApi::class) @Composable fun SearchTopBar( + modifier: Modifier = Modifier, isSearchActive: Boolean, searchBarHint: String, searchQuery: TextFieldValue = TextFieldValue(""), onSearchQueryChanged: (TextFieldValue) -> Unit, onInputClicked: () -> Unit, onCloseSearchClicked: () -> Unit, + shouldRequestFocus: Boolean = false, bottomContent: @Composable ColumnScope.() -> Unit = {} ) { Column( - modifier = Modifier + modifier = modifier .wrapContentHeight() .fillMaxWidth() .background(MaterialTheme.wireColorScheme.background) @@ -117,7 +119,8 @@ fun SearchTopBar( interactionSource = interactionSource, modifier = Modifier .padding(dimensions().spacing8x) - .focusRequester(focusRequester) + .focusRequester(focusRequester), + shouldRequestFocus = shouldRequestFocus ) // We added an invisible clickable box only present when the search is not active. // That way we can still make the whole top bar clickable and intercept and discard the long press gestures. diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/MessageItem.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/MessageItem.kt index ca7dd4d3c21..bac6cd25114 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/MessageItem.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/MessageItem.kt @@ -47,6 +47,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.foundation.layout.FlowRow +import androidx.compose.ui.graphics.Color import com.wire.android.R import com.wire.android.media.audiomessage.AudioState import com.wire.android.model.Clickable @@ -106,7 +107,10 @@ fun MessageItem( onSelfDeletingMessageRead: (UIMessage) -> Unit, onFailedMessageRetryClicked: (String) -> Unit = {}, onFailedMessageCancelClicked: (String) -> Unit = {}, - onLinkClick: (String) -> Unit = {} + onLinkClick: (String) -> Unit = {}, + defaultBackgroundColor: Color = Color.Transparent, + shouldDisplayMessageStatus: Boolean = true, + shouldDisplayFooter: Boolean = true ) { with(message) { val selfDeletionTimerState = rememberSelfDeletionTimer(header.messageStatus.expirationStatus) @@ -133,7 +137,7 @@ fun MessageItem( Modifier.background(color) } else { - Modifier + Modifier.background(defaultBackgroundColor) } Box(backgroundColorModifier) { @@ -236,7 +240,7 @@ fun MessageItem( onLinkClick = onLinkClick ) } - if (isMyMessage) { + if (isMyMessage && shouldDisplayMessageStatus) { MessageStatusIndicator( status = message.header.messageStatus.flowStatus, isGroupConversation = conversationDetailsData is ConversationDetailsData.Group, @@ -249,10 +253,12 @@ fun MessageItem( HorizontalSpace.x24() } } - MessageFooter( - messageFooter, - onReactionClicked - ) + if (shouldDisplayFooter) { + MessageFooter( + messageFooter = messageFooter, + onReactionClicked = onReactionClicked + ) + } } else { MessageDecryptionFailure( messageHeader = header, 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 c210cef85a7..8afbbb5944b 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 @@ -21,9 +21,11 @@ package com.wire.android.ui.home.conversations.details import androidx.annotation.StringRes +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.LocalOverscrollConfiguration -import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyListState @@ -32,6 +34,7 @@ import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect @@ -60,6 +63,7 @@ import com.wire.android.appLogger import com.wire.android.navigation.NavigationCommand import com.wire.android.navigation.Navigator import com.wire.android.navigation.style.PopUpNavigationAnimation +import com.wire.android.ui.common.CollapsingTopBarScaffold import com.wire.android.ui.common.MoreOptionIcon import com.wire.android.ui.common.TabItem import com.wire.android.ui.common.WireTabRow @@ -69,7 +73,6 @@ import com.wire.android.ui.common.bottomsheet.conversation.rememberConversationS import com.wire.android.ui.common.bottomsheet.rememberWireModalSheetState import com.wire.android.ui.common.calculateCurrentTab import com.wire.android.ui.common.dialogs.ArchiveConversationDialog -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 @@ -94,7 +97,9 @@ import com.wire.android.ui.home.conversations.details.participants.GroupConversa import com.wire.android.ui.home.conversations.details.participants.model.UIParticipant import com.wire.android.ui.home.conversationslist.model.DialogState import com.wire.android.ui.home.conversationslist.model.GroupDialogState +import com.wire.android.ui.destinations.SearchConversationMessagesScreenDestination 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.util.ui.UIText import kotlinx.coroutines.launch @@ -116,6 +121,16 @@ fun GroupConversationDetailsScreen( val snackbarHostState = LocalSnackbarHostState.current val showSnackbarMessage: (UIText) -> Unit = remember { { scope.launch { snackbarHostState.showSnackbar(it.asString(resources)) } } } + val onSearchConversationMessagesClick: () -> Unit = { + navigator.navigate( + NavigationCommand( + SearchConversationMessagesScreenDestination( + conversationId = viewModel.conversationId + ) + ) + ) + } + GroupConversationDetailsContent( conversationSheetContent = viewModel.conversationSheetContent, bottomSheetEventsHandler = viewModel, @@ -195,7 +210,8 @@ fun GroupConversationDetailsScreen( onEditGroupName = { navigator.navigate(NavigationCommand(EditConversationNameScreenDestination(viewModel.conversationId))) }, - isLoading = viewModel.requestInProgress + isLoading = viewModel.requestInProgress, + onSearchConversationMessagesClick = onSearchConversationMessagesClick ) val tryAgainSnackBarMessage = stringResource(id = R.string.error_unknown_message) @@ -235,7 +251,8 @@ private fun GroupConversationDetailsContent( onLeaveGroup: (GroupDialogState) -> Unit, onDeleteGroup: (GroupDialogState) -> Unit, groupParticipantsState: GroupConversationParticipantsState, - isLoading: Boolean + isLoading: Boolean, + onSearchConversationMessagesClick: () -> Unit ) { val scope = rememberCoroutineScope() val resources = LocalContext.current.resources @@ -281,64 +298,87 @@ private fun GroupConversationDetailsContent( clearConversationDialogState.dismiss() archiveConversationDialogState.dismiss() } - WireScaffold( - topBar = { + + CollapsingTopBarScaffold( + topBarHeader = { WireCenterAlignedTopAppBar( elevation = elevationState, title = stringResource(R.string.conversation_details_title), navigationIconType = NavigationIconType.Close, onNavigationPressed = onBackPressed, actions = { MoreOptionIcon(onButtonClicked = openBottomSheet) } - ) { - WireTabRow( - tabs = GroupConversationDetailsTabItem.entries, - selectedTabIndex = currentTabState, - onTabChange = { scope.launch { pagerState.animateScrollToPage(it) } }, - modifier = Modifier.padding(top = MaterialTheme.wireDimensions.spacing16x), - divider = {} // no divider + ) + }, + topBarCollapsing = { + conversationSheetState.conversationSheetContent?.let { + GroupConversationDetailsTopBarCollapsing( + title = it.title, + conversationId = it.conversationId, + totalParticipants = groupParticipantsState.data.allCount, + isLoading = isLoading, + onSearchConversationMessagesClick = onSearchConversationMessagesClick ) } }, - modifier = Modifier.fillMaxHeight(), - ) { internalPadding -> - var focusedTabIndex: Int by remember { mutableStateOf(initialPageIndex) } - val keyboardController = LocalSoftwareKeyboardController.current - val focusManager = LocalFocusManager.current - - CompositionLocalProvider(LocalOverscrollConfiguration provides null) { - HorizontalPager( - state = pagerState, - modifier = Modifier - .fillMaxWidth() - .padding(internalPadding) - ) { pageIndex -> - when (GroupConversationDetailsTabItem.entries[pageIndex]) { - GroupConversationDetailsTabItem.OPTIONS -> GroupConversationOptions( - lazyListState = lazyListStates[pageIndex], - onEditGuestAccess = onEditGuestAccess, - onEditSelfDeletingMessages = onEditSelfDeletingMessages, - onEditGroupName = onEditGroupName - ) - - GroupConversationDetailsTabItem.PARTICIPANTS -> GroupConversationParticipants( - groupParticipantsState = groupParticipantsState, - openFullListPressed = openFullListPressed, - onAddParticipantsPressed = onAddParticipantsPressed, - onProfilePressed = onProfilePressed, - lazyListState = lazyListStates[pageIndex] + topBarFooter = { + AnimatedVisibility( + visible = conversationSheetState.conversationSheetContent != null, + enter = fadeIn(), + exit = fadeOut(), + ) { + Surface( + shadowElevation = elevationState, + color = MaterialTheme.wireColorScheme.background + ) { + WireTabRow( + tabs = GroupConversationDetailsTabItem.entries, + selectedTabIndex = currentTabState, + onTabChange = { scope.launch { pagerState.animateScrollToPage(it) } }, + modifier = Modifier.padding(top = MaterialTheme.wireDimensions.spacing16x), + divider = {} // no divider ) } } + }, + content = { + var focusedTabIndex: Int by remember { mutableStateOf(initialPageIndex) } + val keyboardController = LocalSoftwareKeyboardController.current + val focusManager = LocalFocusManager.current + + CompositionLocalProvider(LocalOverscrollConfiguration provides null) { + HorizontalPager( + state = pagerState, + modifier = Modifier + .fillMaxWidth() + ) { pageIndex -> + when (GroupConversationDetailsTabItem.entries[pageIndex]) { + GroupConversationDetailsTabItem.OPTIONS -> GroupConversationOptions( + lazyListState = lazyListStates[pageIndex], + onEditGuestAccess = onEditGuestAccess, + onEditSelfDeletingMessages = onEditSelfDeletingMessages, + onEditGroupName = onEditGroupName + ) - LaunchedEffect(pagerState.isScrollInProgress, focusedTabIndex, pagerState.currentPage) { - if (!pagerState.isScrollInProgress && focusedTabIndex != pagerState.currentPage) { - keyboardController?.hide() - focusManager.clearFocus() - focusedTabIndex = pagerState.currentPage + GroupConversationDetailsTabItem.PARTICIPANTS -> GroupConversationParticipants( + groupParticipantsState = groupParticipantsState, + openFullListPressed = openFullListPressed, + onAddParticipantsPressed = onAddParticipantsPressed, + onProfilePressed = onProfilePressed, + lazyListState = lazyListStates[pageIndex] + ) + } + } + + LaunchedEffect(pagerState.isScrollInProgress, focusedTabIndex, pagerState.currentPage) { + if (!pagerState.isScrollInProgress && focusedTabIndex != pagerState.currentPage) { + keyboardController?.hide() + focusManager.clearFocus() + focusedTabIndex = pagerState.currentPage + } } } } - } + ) WireModalSheetLayout( sheetState = sheetState, @@ -428,7 +468,8 @@ fun PreviewGroupConversationDetails() { isLoading = false, onEditGroupName = {}, onEditSelfDeletingMessages = {}, - onEditGuestAccess = {} + onEditGuestAccess = {}, + onSearchConversationMessagesClick = {} ) } } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsTopBarCollapsing.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsTopBarCollapsing.kt new file mode 100644 index 00000000000..d2cf6f777da --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsTopBarCollapsing.kt @@ -0,0 +1,130 @@ +/* + * Wire + * Copyright (C) 2023 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.android.ui.home.conversations.details + +import androidx.compose.animation.animateContentSize +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.constraintlayout.compose.ConstraintLayout +import com.wire.android.R +import com.wire.android.ui.common.colorsScheme +import com.wire.android.ui.common.conversationColor +import com.wire.android.ui.common.dimensions +import com.wire.android.ui.home.conversations.search.messages.SearchConversationMessagesButton +import com.wire.android.ui.home.conversationslist.common.GroupConversationAvatar +import com.wire.android.ui.theme.wireColorScheme +import com.wire.android.ui.theme.wireTypography +import com.wire.android.util.ui.UIText +import com.wire.kalium.logic.data.id.ConversationId + +@Composable +fun GroupConversationDetailsTopBarCollapsing( + title: String, + conversationId: ConversationId, + totalParticipants: Int, + isLoading: Boolean, + onSearchConversationMessagesClick: () -> Unit, + modifier: Modifier = Modifier +) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + modifier = modifier + .fillMaxWidth() + .wrapContentHeight() + ) { + Box(contentAlignment = Alignment.Center) { + GroupConversationAvatar( + color = colorsScheme().conversationColor(id = conversationId), + size = dimensions().groupAvatarConversationDetailsTopBarSize, + cornerRadius = dimensions().groupAvatarConversationDetailsCornerRadius, + padding = dimensions().avatarConversationTopBarClickablePadding, + ) + } + ConstraintLayout( + Modifier + .fillMaxWidth() + .wrapContentHeight() + .animateContentSize() + ) { + val (userDescription) = createRefs() + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .wrapContentSize() + .constrainAs(userDescription) { + top.linkTo(parent.top) + bottom.linkTo(parent.bottom) + start.linkTo(parent.start) + end.linkTo(parent.end) + } + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .padding( + horizontal = dimensions().spacing16x, + vertical = dimensions().spacing4x + ) + ) { + Text( + text = title.ifBlank { + if (isLoading) "" + else UIText.StringResource(R.string.group_unavailable_label).asString() + }, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + style = MaterialTheme.wireTypography.body02, + color = MaterialTheme.colorScheme.onBackground + ) + } + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .padding(horizontal = dimensions().spacing64x) + ) { + Text( + text = stringResource( + id = R.string.conversation_details_participants_count, + totalParticipants + ), + style = MaterialTheme.wireTypography.subline01, + color = MaterialTheme.wireColorScheme.secondaryText + ) + } + } + } + + SearchConversationMessagesButton( + onSearchConversationMessagesClick = onSearchConversationMessagesClick + ) + } +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchBarConversationMessagesState.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchBarConversationMessagesState.kt new file mode 100644 index 00000000000..fbceb269f3a --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchBarConversationMessagesState.kt @@ -0,0 +1,75 @@ +/* + * Wire + * Copyright (C) 2023 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.android.ui.home.conversations.search.messages + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.text.input.TextFieldValue + +@Composable +fun rememberSearchBarConversationMessagesState(): SearchBarConversationMessagesState { + val searchBarState = rememberSaveable( + saver = SearchBarConversationMessagesState.saver() + ) { + SearchBarConversationMessagesState() + } + + return searchBarState +} + +class SearchBarConversationMessagesState( + isSearchActive: Boolean = false, + searchQuery: TextFieldValue = TextFieldValue("") +) { + + var isSearchActive by mutableStateOf(isSearchActive) + private set + + var searchQuery by mutableStateOf(searchQuery) + + fun closeSearch() { + isSearchActive = false + } + + fun openSearch() { + isSearchActive = true + } + + fun searchQueryChanged(searchQuery: TextFieldValue) { + this.searchQuery = searchQuery + } + + companion object { + + fun saver(): Saver = Saver( + save = { + listOf(it.isSearchActive, it.searchQuery.text) + }, + restore = { + SearchBarConversationMessagesState( + isSearchActive = it[0] as Boolean, + searchQuery = TextFieldValue(it[1] as String) + ) + } + ) + } +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesButton.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesButton.kt new file mode 100644 index 00000000000..bf09fee6a11 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesButton.kt @@ -0,0 +1,73 @@ +/* + * Wire + * Copyright (C) 2023 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.android.ui.home.conversations.search.messages + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.DpSize +import com.wire.android.R +import com.wire.android.ui.common.button.WireSecondaryButton +import com.wire.android.ui.common.dimensions + +@Composable +fun SearchConversationMessagesButton( + onSearchConversationMessagesClick: () -> Unit +) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .wrapContentSize() + .fillMaxWidth() + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .padding( + top = dimensions().spacing24x, + start = dimensions().spacing16x, + end = dimensions().spacing16x, + ) + .fillMaxWidth() + ) { + WireSecondaryButton( + text = stringResource(R.string.label_search_button), + onClick = onSearchConversationMessagesClick, + minSize = DpSize(dimensions().spacing0x, dimensions().spacing48x), + fillMaxWidth = true, + leadingIcon = { + Icon( + painter = painterResource(id = R.drawable.ic_search), + contentDescription = stringResource(R.string.label_search_messages), + tint = MaterialTheme.colorScheme.onBackground, + modifier = Modifier.padding(end = dimensions().spacing8x) + ) + } + ) + } + } +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesEmptyScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesEmptyScreen.kt new file mode 100644 index 00000000000..3397a2cfc8b --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesEmptyScreen.kt @@ -0,0 +1,89 @@ +/* + * Wire + * Copyright (C) 2023 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.android.ui.home.conversations.search.messages + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextDecoration +import com.wire.android.BuildConfig +import com.wire.android.R +import com.wire.android.ui.common.dimensions +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.CustomTabsHelper +import com.wire.android.util.ui.PreviewMultipleThemes + +@Composable +fun SearchConversationMessagesEmptyScreen() { + val context = LocalContext.current + Box(modifier = Modifier.fillMaxSize()) { + Column( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Spacer(modifier = Modifier.height(dimensions().spacing16x)) + Text( + text = stringResource(R.string.label_search_messages_empty_title), + style = MaterialTheme.wireTypography.body01.copy(color = MaterialTheme.wireColorScheme.secondaryText), + textAlign = TextAlign.Center + ) + Spacer(modifier = Modifier.height(dimensions().spacing8x)) + Text( + text = stringResource(R.string.label_learn_more), + style = MaterialTheme.wireTypography.body02.copy( + textDecoration = TextDecoration.Underline, + color = MaterialTheme.colorScheme.primary + ), + modifier = Modifier.clickable { + CustomTabsHelper.launchUrl( + context, + LEARN_ABOUT_SEARCH_URL + ) + } + ) + } + } +} + +private const val LEARN_ABOUT_SEARCH_URL = + "${BuildConfig.URL_SUPPORT}/hc/en-us/articles/115001426529-Search-in-a-conversation" + +@PreviewMultipleThemes +@Composable +fun previewSearchConversationMessagesEmptyScreen() { + WireTheme { + SearchConversationMessagesEmptyScreen() + } +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesNavArgs.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesNavArgs.kt new file mode 100644 index 00000000000..cc96a1bbe68 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesNavArgs.kt @@ -0,0 +1,24 @@ +/* + * Wire + * Copyright (C) 2023 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.android.ui.home.conversations.search.messages + +import com.wire.kalium.logic.data.id.ConversationId + +data class SearchConversationMessagesNavArgs( + val conversationId: ConversationId +) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesNoResultsScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesNoResultsScreen.kt new file mode 100644 index 00000000000..a33bc202ab4 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesNoResultsScreen.kt @@ -0,0 +1,66 @@ +/* + * Wire + * Copyright (C) 2023 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.android.ui.home.conversations.search.messages + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import com.wire.android.R +import com.wire.android.ui.common.dimensions +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.ui.PreviewMultipleThemes + +@Composable +fun SearchConversationMessagesNoResultsScreen() { + Box(modifier = Modifier.fillMaxSize()) { + Column( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Spacer(modifier = Modifier.height(dimensions().spacing16x)) + Text( + text = stringResource(R.string.label_search_messages_no_results), + style = MaterialTheme.wireTypography.body01.copy(color = MaterialTheme.wireColorScheme.secondaryText), + textAlign = TextAlign.Center + ) + } + } +} + +@PreviewMultipleThemes +@Composable +fun previewSearchConversationMessagesNoResultsScreen() { + WireTheme { + SearchConversationMessagesNoResultsScreen() + } +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesResultsScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesResultsScreen.kt new file mode 100644 index 00000000000..0f3c65a04f6 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesResultsScreen.kt @@ -0,0 +1,74 @@ +/* + * Wire + * Copyright (C) 2023 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.android.ui.home.conversations.search.messages + +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.runtime.Composable +import com.wire.android.ui.common.colorsScheme +import com.wire.android.ui.home.conversations.MessageItem +import com.wire.android.ui.home.conversations.info.ConversationDetailsData +import com.wire.android.ui.home.conversations.mock.mockMessageWithText +import com.wire.android.ui.home.conversations.model.UIMessage +import com.wire.android.ui.theme.WireTheme +import com.wire.android.util.ui.PreviewMultipleThemes + +@Composable +fun SearchConversationMessagesResultsScreen( + searchResult: List +) { + LazyColumn { + items(searchResult) { message -> + when (message) { + is UIMessage.Regular -> { + MessageItem( + message = message, + conversationDetailsData = ConversationDetailsData.None, + audioMessagesState = mapOf(), + onLongClicked = { }, + onAssetMessageClicked = { }, + onAudioClick = { }, + onChangeAudioPosition = { _, _ -> }, + onImageMessageClicked = { _, _ -> }, + onOpenProfile = { }, + onReactionClicked = { _, _ -> }, + onResetSessionClicked = { _, _ -> }, + onSelfDeletingMessageRead = { }, + defaultBackgroundColor = colorsScheme().backgroundVariant, + shouldDisplayMessageStatus = false, + shouldDisplayFooter = false + ) + } + is UIMessage.System -> { } + } + } + } +} + +@PreviewMultipleThemes +@Composable +fun previewSearchConversationMessagesResultsScreen() { + WireTheme { + SearchConversationMessagesResultsScreen( + searchResult = listOf( + mockMessageWithText, + mockMessageWithText, + ) + ) + } +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesScreen.kt new file mode 100644 index 00000000000..ee3d736af2b --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesScreen.kt @@ -0,0 +1,106 @@ +/* + * Wire + * Copyright (C) 2023 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.android.ui.home.conversations.search.messages + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +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.navigation.Navigator +import com.wire.android.navigation.style.PopUpNavigationAnimation +import com.wire.android.ui.common.CollapsingTopBarScaffold +import com.wire.android.ui.common.dimensions +import com.wire.android.ui.common.topappbar.search.SearchTopBar +import com.wire.android.ui.home.conversations.model.UIMessage + +@RootNavGraph +@Destination( + navArgsDelegate = SearchConversationMessagesNavArgs::class, + style = PopUpNavigationAnimation::class +) +@Composable +fun SearchConversationMessagesScreen( + navigator: Navigator, + searchConversationMessagesViewModel: SearchConversationMessagesViewModel = hiltViewModel() +) { + val searchBarState = rememberSearchBarConversationMessagesState() + + with(searchConversationMessagesViewModel.searchConversationMessagesState) { + CollapsingTopBarScaffold( + topBarHeader = { }, + topBarCollapsing = { + val onInputClicked: () -> Unit = remember { { searchBarState.openSearch() } } + val onCloseSearchClicked: () -> Unit = remember { + { + searchBarState.closeSearch() + navigator.navigateBack() + } + } + + SearchTopBar( + isSearchActive = searchBarState.isSearchActive, + searchBarHint = stringResource(id = R.string.label_search_messages), + searchQuery = searchQuery, + onSearchQueryChanged = searchConversationMessagesViewModel::searchQueryChanged, + onInputClicked = onInputClicked, + onCloseSearchClicked = onCloseSearchClicked, + modifier = Modifier.padding(top = dimensions().spacing24x), + shouldRequestFocus = true + ) + }, + content = { + SearchConversationMessagesResultContent( + searchQuery = searchQuery.text, + noneSearchSucceed = isEmptyResult, + searchResult = searchResult + ) + BackHandler(enabled = searchBarState.isSearchActive) { + searchBarState.closeSearch() + } + }, + bottomBar = { }, + snapOnFling = false, + keepElevationWhenCollapsed = true + ) + } +} + +@Composable +fun SearchConversationMessagesResultContent( + searchQuery: String, + noneSearchSucceed: Boolean, + searchResult: List +) { + if (searchQuery.isEmpty()) { + SearchConversationMessagesEmptyScreen() + } else { + if (noneSearchSucceed) { + SearchConversationMessagesNoResultsScreen() + } else { + SearchConversationMessagesResultsScreen( + searchResult = searchResult + ) + } + } +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesState.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesState.kt new file mode 100644 index 00000000000..43fa32f70b5 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesState.kt @@ -0,0 +1,29 @@ +/* + * Wire + * Copyright (C) 2023 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.android.ui.home.conversations.search.messages + +import androidx.compose.ui.text.input.TextFieldValue +import com.wire.android.ui.home.conversations.model.UIMessage +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +data class SearchConversationMessagesState( + val searchQuery: TextFieldValue = TextFieldValue(""), + val searchResult: ImmutableList = persistentListOf(), + val isEmptyResult: Boolean = false +) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesViewModel.kt new file mode 100644 index 00000000000..dddd99b1410 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesViewModel.kt @@ -0,0 +1,92 @@ +/* + * Wire + * Copyright (C) 2023 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.android.ui.home.conversations.search.messages + +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.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.wire.android.ui.home.conversations.search.SearchPeopleViewModel +import com.wire.android.ui.home.conversations.usecase.GetConversationMessagesFromSearchUseCase +import com.wire.android.ui.navArgs +import com.wire.kalium.logic.data.id.QualifiedID +import com.wire.kalium.logic.functional.onSuccess +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.collections.immutable.toPersistentList +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class SearchConversationMessagesViewModel @Inject constructor( + private val getSearchMessagesForConversation: GetConversationMessagesFromSearchUseCase, + savedStateHandle: SavedStateHandle +) : ViewModel() { + + private val searchConversationMessagesNavArgs: SearchConversationMessagesNavArgs = savedStateHandle.navArgs() + private val conversationId: QualifiedID = searchConversationMessagesNavArgs.conversationId + + var searchConversationMessagesState by mutableStateOf(SearchConversationMessagesState()) + + private val mutableSearchQueryFlow = MutableStateFlow("") + private val searchQueryTextFieldFlow = MutableStateFlow(TextFieldValue("")) + + private val searchQueryFlow = mutableSearchQueryFlow + .asStateFlow() + .debounce(SearchPeopleViewModel.DEFAULT_SEARCH_QUERY_DEBOUNCE) + + init { + viewModelScope.launch { + searchQueryTextFieldFlow.collect { + searchConversationMessagesState = searchConversationMessagesState.copy( + searchQuery = it + ) + } + } + viewModelScope.launch { + searchQueryFlow + .collectLatest { searchTerm -> + getSearchMessagesForConversation( + searchTerm = searchTerm, + conversationId = conversationId + ).onSuccess { uiMessages -> + searchConversationMessagesState = searchConversationMessagesState.copy( + searchResult = uiMessages.toPersistentList(), + isEmptyResult = uiMessages.isEmpty() + ) + } + } + } + } + + fun searchQueryChanged(searchQuery: TextFieldValue) { + val textQueryChanged = searchQueryTextFieldFlow.value.text != searchQuery.text + // we set the state with a searchQuery, immediately to update the UI first + viewModelScope.launch { + searchQueryTextFieldFlow.emit(searchQuery) + + if (textQueryChanged) mutableSearchQueryFlow.emit(searchQuery.text) + } + } +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/GetConversationMessagesFromSearchUseCase.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/GetConversationMessagesFromSearchUseCase.kt new file mode 100644 index 00000000000..716a98e1da4 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/GetConversationMessagesFromSearchUseCase.kt @@ -0,0 +1,73 @@ +/* + * Wire + * Copyright (C) 2023 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.android.ui.home.conversations.usecase + +import com.wire.android.mapper.MessageMapper +import com.wire.android.ui.home.conversations.model.UIMessage +import com.wire.kalium.logic.CoreFailure +import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.feature.conversation.ObserveUserListByIdUseCase +import com.wire.kalium.logic.feature.message.GetConversationMessagesFromSearchQueryUseCase +import com.wire.kalium.logic.functional.Either +import com.wire.kalium.logic.functional.map +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +class GetConversationMessagesFromSearchUseCase @Inject constructor( + private val getConversationMessagesFromSearch: GetConversationMessagesFromSearchQueryUseCase, + private val observeMemberDetailsByIds: ObserveUserListByIdUseCase, + private val messageMapper: MessageMapper +) { + + /** + * This operation combines messages searched from a conversation and its respective user to UI + * @param searchQuery The search term used to define which messages will be returned. + * @param conversationId The conversation ID that it will look for messages in. + * @return A [Either>] indicating the success of the operation. + */ + suspend operator fun invoke( + searchTerm: String, + conversationId: ConversationId + ): Either> = + if (searchTerm.length >= MINIMUM_CHARACTERS_TO_SEARCH) { + getConversationMessagesFromSearch( + searchQuery = searchTerm, + conversationId = conversationId + ).map { foundMessages -> + foundMessages.flatMap { messageItem -> + observeMemberDetailsByIds( + userIdList = messageMapper.memberIdList( + messages = foundMessages + ) + ).map { usersList -> + messageMapper.toUIMessage( + userList = usersList, + message = messageItem + )?.let { listOf(it) } ?: emptyList() + }.first() + } + } + } else { + Either.Right(value = listOf()) + } + + private companion object { + const val MINIMUM_CHARACTERS_TO_SEARCH = 2 + } +} diff --git a/app/src/main/kotlin/com/wire/android/ui/theme/WireDimensions.kt b/app/src/main/kotlin/com/wire/android/ui/theme/WireDimensions.kt index fce801fd49d..6c00ead58df 100644 --- a/app/src/main/kotlin/com/wire/android/ui/theme/WireDimensions.kt +++ b/app/src/main/kotlin/com/wire/android/ui/theme/WireDimensions.kt @@ -46,6 +46,8 @@ data class WireDimensions( val groupAvatarCornerRadius: Dp, val avatarConversationTopBarSize: Dp, val groupAvatarConversationTopBarCornerRadius: Dp, + val groupAvatarConversationDetailsTopBarSize: Dp, + val groupAvatarConversationDetailsCornerRadius: Dp, val avatarConversationTopBarClickablePadding: Dp, // Drawer Navigation val homeDrawerHorizontalPadding: Dp, @@ -210,6 +212,8 @@ private val DefaultPhonePortraitWireDimensions: WireDimensions = WireDimensions( groupAvatarCornerRadius = 10.dp, avatarConversationTopBarSize = 24.dp, groupAvatarConversationTopBarCornerRadius = 8.dp, + groupAvatarConversationDetailsTopBarSize = 64.dp, + groupAvatarConversationDetailsCornerRadius = 20.dp, avatarConversationTopBarClickablePadding = 2.dp, homeDrawerHorizontalPadding = 8.dp, homeDrawerBottomPadding = 16.dp, diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/common/UserProfileInfo.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/common/UserProfileInfo.kt index ecfa398f3e6..c45204efa01 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/common/UserProfileInfo.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/common/UserProfileInfo.kt @@ -62,6 +62,7 @@ import com.wire.android.ui.common.UserProfileAvatar import com.wire.android.ui.common.banner.SecurityClassificationBannerForUser import com.wire.android.ui.common.dimensions import com.wire.android.ui.common.progress.WireCircularProgressIndicator +import com.wire.android.ui.home.conversations.search.messages.SearchConversationMessagesButton import com.wire.android.ui.home.conversationslist.model.Membership import com.wire.android.ui.theme.wireColorScheme import com.wire.android.ui.theme.wireTypography @@ -88,7 +89,9 @@ fun UserProfileInfo( modifier: Modifier = Modifier, connection: ConnectionState = ConnectionState.ACCEPTED, delayToShowPlaceholderIfNoAsset: Duration = 200.milliseconds, - isProteusVerified: Boolean = false + isProteusVerified: Boolean = false, + onSearchConversationMessagesClick: () -> Unit = {}, + shouldShowSearchButton: Boolean = false ) { Column( horizontalAlignment = CenterHorizontally, @@ -227,6 +230,12 @@ fun UserProfileInfo( modifier = Modifier.padding(top = dimensions().spacing8x) ) } + + if (shouldShowSearchButton) { + SearchConversationMessagesButton( + onSearchConversationMessagesClick = onSearchConversationMessagesClick + ) + } } } diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreen.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreen.kt index c7d3e2c0480..9c2433a32a5 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreen.kt @@ -89,6 +89,7 @@ import com.wire.android.ui.common.visbility.rememberVisibilityState import com.wire.android.ui.connection.ConnectionActionButton import com.wire.android.ui.destinations.ConversationScreenDestination import com.wire.android.ui.destinations.DeviceDetailsScreenDestination +import com.wire.android.ui.destinations.SearchConversationMessagesScreenDestination import com.wire.android.ui.home.conversations.details.dialog.ClearConversationContentDialog import com.wire.android.ui.home.conversationslist.model.DialogState import com.wire.android.ui.home.conversationslist.model.Membership @@ -128,6 +129,21 @@ fun OtherUserProfileScreen( val openBottomSheet: () -> Unit = remember { { scope.launch { sheetState.show() } } } val closeBottomSheet: () -> Unit = remember { { scope.launch { sheetState.hide() } } } + val conversationId = viewModel.state.conversationId + ?: viewModel.state.conversationSheetContent?.conversationId + val shouldShowSearchButton = viewModel.shouldShowSearchButton(conversationId = conversationId) + val onSearchConversationMessagesClick: () -> Unit = { + conversationId?.let { + navigator.navigate( + NavigationCommand( + SearchConversationMessagesScreenDestination( + conversationId = it + ) + ) + ) + } + } + OtherProfileScreenContent( scope = scope, state = viewModel.state, @@ -143,6 +159,8 @@ fun OtherUserProfileScreen( }, onOpenConversation = { navigator.navigate(NavigationCommand(ConversationScreenDestination(it), BackStackMode.UPDATE_EXISTED)) }, onOpenDeviceDetails = { navigator.navigate(NavigationCommand(DeviceDetailsScreenDestination(navArgs.userId, it.clientId))) }, + onSearchConversationMessagesClick = onSearchConversationMessagesClick, + shouldShowSearchButton = shouldShowSearchButton, navigateBack = navigator::navigateBack, navigationIconType = NavigationIconType.Close, ) @@ -175,6 +193,8 @@ fun OtherProfileScreenContent( onIgnoreConnectionRequest: (String) -> Unit = { }, onOpenConversation: (ConversationId) -> Unit = {}, onOpenDeviceDetails: (Device) -> Unit = {}, + onSearchConversationMessagesClick: () -> Unit, + shouldShowSearchButton: Boolean, navigateBack: () -> Unit = {} ) { val otherUserProfileScreenState = rememberOtherUserProfileScreenState() @@ -249,7 +269,13 @@ fun OtherProfileScreenContent( openConversationBottomSheet = openConversationBottomSheet ) }, - topBarCollapsing = { TopBarCollapsing(state) }, + topBarCollapsing = { + TopBarCollapsing( + state = state, + onSearchConversationMessagesClick = onSearchConversationMessagesClick, + shouldShowSearchButton = shouldShowSearchButton + ) + }, topBarFooter = { TopBarFooter(state, pagerState, tabBarElevationState, tabItems, currentTabState, scope) }, content = { Content( @@ -345,8 +371,15 @@ private fun TopBarHeader( } @Composable -private fun TopBarCollapsing(state: OtherUserProfileState) { - Crossfade(targetState = state, label = "OtherUserProfileScreenTopBarCollapsing") { targetState -> +private fun TopBarCollapsing( + state: OtherUserProfileState, + onSearchConversationMessagesClick: () -> Unit, + shouldShowSearchButton: Boolean +) { + Crossfade( + targetState = state, + label = "OtherUserProfileScreenTopBarCollapsing" + ) { targetState -> UserProfileInfo( userId = targetState.userId, isLoading = targetState.isAvatarLoading, @@ -358,7 +391,9 @@ private fun TopBarCollapsing(state: OtherUserProfileState) { editableState = EditableState.NotEditable, modifier = Modifier.padding(bottom = dimensions().spacing16x), connection = targetState.connectionState, - isProteusVerified = targetState.isProteusVerified + isProteusVerified = targetState.isProteusVerified, + onSearchConversationMessagesClick = onSearchConversationMessagesClick, + shouldShowSearchButton = shouldShowSearchButton ) } } @@ -505,13 +540,17 @@ enum class OtherUserProfileTabItem(@StringRes override val titleResId: Int) : Ta fun PreviewOtherProfileScreenContent() { WireTheme(isPreview = true) { OtherProfileScreenContent( - rememberCoroutineScope(), - OtherUserProfileState.PREVIEW.copy(connectionState = ConnectionState.ACCEPTED), - NavigationIconType.Back, - false, - rememberWireModalSheetState(), - {}, {}, OtherUserProfileEventsHandler.PREVIEW, - OtherUserProfileBottomSheetEventsHandler.PREVIEW + scope = rememberCoroutineScope(), + state = OtherUserProfileState.PREVIEW.copy(connectionState = ConnectionState.ACCEPTED), + navigationIconType = NavigationIconType.Back, + requestInProgress = false, + sheetState = rememberWireModalSheetState(), + openBottomSheet = {}, + closeBottomSheet = {}, + eventsHandler = OtherUserProfileEventsHandler.PREVIEW, + bottomSheetEventsHandler = OtherUserProfileBottomSheetEventsHandler.PREVIEW, + onSearchConversationMessagesClick = {}, + shouldShowSearchButton = false ) } } @@ -522,13 +561,17 @@ fun PreviewOtherProfileScreenContent() { fun PreviewOtherProfileScreenContentNotConnected() { WireTheme(isPreview = true) { OtherProfileScreenContent( - rememberCoroutineScope(), - OtherUserProfileState.PREVIEW.copy(connectionState = ConnectionState.CANCELLED), - NavigationIconType.Back, - false, - rememberWireModalSheetState(), - {}, {}, OtherUserProfileEventsHandler.PREVIEW, - OtherUserProfileBottomSheetEventsHandler.PREVIEW, + scope = rememberCoroutineScope(), + state = OtherUserProfileState.PREVIEW.copy(connectionState = ConnectionState.CANCELLED), + navigationIconType = NavigationIconType.Back, + requestInProgress = false, + sheetState = rememberWireModalSheetState(), + openBottomSheet = {}, + closeBottomSheet = {}, + eventsHandler = OtherUserProfileEventsHandler.PREVIEW, + bottomSheetEventsHandler = OtherUserProfileBottomSheetEventsHandler.PREVIEW, + onSearchConversationMessagesClick = {}, + shouldShowSearchButton = false ) } } diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreenViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreenViewModel.kt index c6cc450170b..4e96648dd30 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreenViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreenViewModel.kt @@ -54,6 +54,7 @@ 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.QualifiedID +import com.wire.kalium.logic.data.user.ConnectionState import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.feature.client.ObserveClientsByUserIdUseCase import com.wire.kalium.logic.feature.client.PersistOtherUserClientsUseCase @@ -384,4 +385,11 @@ class OtherUserProfileScreenViewModel @Inject constructor( } ) } + + fun shouldShowSearchButton(conversationId: ConversationId?): Boolean = + conversationId != null && state.connectionState in listOf( + ConnectionState.ACCEPTED, + ConnectionState.BLOCKED, + ConnectionState.MISSING_LEGALHOLD_CONSENT + ) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9c35b0067d6..8c336fbdd37 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -417,6 +417,7 @@ you Add Participants Name not available + Group name not available Conversation Details OPTIONS PARTICIPANTS @@ -424,6 +425,7 @@ GROUP ADMINS (%d) GROUP MEMBERS (%d) This group has %s participants.\nUp to 500 people can join a group conversation. + %s participants Show all participants (%d) Group Participants Add participants @@ -680,7 +682,12 @@ %s is typing %1$s and %2$d more are typing - + + Search messages + Search all messages in this conversation. + No results could be found.\nPlease refine your search and try again. + Search + CONTACTS New Group New Conversation diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/messages/SearchConversationMessagesViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/messages/SearchConversationMessagesViewModelTest.kt new file mode 100644 index 00000000000..de0128d8fd0 --- /dev/null +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/messages/SearchConversationMessagesViewModelTest.kt @@ -0,0 +1,135 @@ +/* + * Wire + * Copyright (C) 2023 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.android.ui.home.conversations.messages + +import androidx.compose.ui.text.input.TextFieldValue +import androidx.lifecycle.SavedStateHandle +import com.wire.android.config.CoroutineTestExtension +import com.wire.android.config.NavigationTestExtension +import com.wire.android.config.mockUri +import com.wire.android.ui.home.conversations.mock.mockMessageWithText +import com.wire.android.ui.home.conversations.model.MessageBody +import com.wire.android.ui.home.conversations.model.UIMessage +import com.wire.android.ui.home.conversations.model.UIMessageContent +import com.wire.android.ui.home.conversations.search.messages.SearchConversationMessagesNavArgs +import com.wire.android.ui.home.conversations.search.messages.SearchConversationMessagesViewModel +import com.wire.android.ui.home.conversations.usecase.GetConversationMessagesFromSearchUseCase +import com.wire.android.ui.navArgs +import com.wire.android.util.ui.UIText +import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.functional.Either +import io.mockk.MockKAnnotations +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.impl.annotations.MockK +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest +import org.amshove.kluent.internal.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith + +@OptIn(ExperimentalCoroutinesApi::class) +@ExtendWith(CoroutineTestExtension::class) +@ExtendWith(NavigationTestExtension::class) +class SearchConversationMessagesViewModelTest { + + @Test + fun `given search term, when searching for messages, then specific messages are returned`() = + runTest() { + // given + val searchTerm = "message" + val messages = listOf( + mockMessageWithText.copy( + messageContent = UIMessageContent.TextMessage( + messageBody = MessageBody( + UIText.DynamicString("message1") + ) + ) + ), + mockMessageWithText.copy( + messageContent = UIMessageContent.TextMessage( + messageBody = MessageBody( + UIText.DynamicString("message2") + ) + ) + ) + ) + + val (arrangement, viewModel) = SearchConversationMessagesViewModelArrangement() + .withSuccessSearch(searchTerm, messages) + .arrange() + + // when + viewModel.searchQueryChanged(TextFieldValue(searchTerm)) + advanceUntilIdle() + + // then + assertEquals( + TextFieldValue(searchTerm), + viewModel.searchConversationMessagesState.searchQuery + ) + coVerify(exactly = 1) { + arrangement.getSearchMessagesForConversation( + searchTerm, + arrangement.conversationId + ) + } + } + + class SearchConversationMessagesViewModelArrangement { + val conversationId: ConversationId = ConversationId( + value = "some-dummy-value", + domain = "some-dummy-domain" + ) + + @MockK + private lateinit var savedStateHandle: SavedStateHandle + + @MockK + lateinit var getSearchMessagesForConversation: GetConversationMessagesFromSearchUseCase + + private val viewModel: SearchConversationMessagesViewModel by lazy { + SearchConversationMessagesViewModel( + getSearchMessagesForConversation = getSearchMessagesForConversation, + savedStateHandle = savedStateHandle + ) + } + + init { + // Tests setup + MockKAnnotations.init(this, relaxUnitFun = true) + mockUri() + every { savedStateHandle.navArgs() } returns SearchConversationMessagesNavArgs( + conversationId = conversationId + ) + } + + suspend fun withSuccessSearch( + searchTerm: String, + messages: List + ) = apply { + coEvery { + getSearchMessagesForConversation(eq(searchTerm), eq(conversationId)) + } returns Either.Right(messages) + } + + fun arrange() = this to viewModel + } +} diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/usecase/GetConversationMessagesFromSearchUseCaseTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/usecase/GetConversationMessagesFromSearchUseCaseTest.kt new file mode 100644 index 00000000000..f52b90449dc --- /dev/null +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/usecase/GetConversationMessagesFromSearchUseCaseTest.kt @@ -0,0 +1,194 @@ +/* + * Wire + * Copyright (C) 2023 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.android.ui.home.conversations.usecase + +import com.wire.android.config.CoroutineTestExtension +import com.wire.android.framework.TestMessage +import com.wire.android.framework.TestUser +import com.wire.android.mapper.MessageMapper +import com.wire.android.model.UserAvatarData +import com.wire.android.ui.home.conversations.model.MessageBody +import com.wire.android.ui.home.conversations.model.MessageSource +import com.wire.android.ui.home.conversations.model.UIMessage +import com.wire.android.ui.home.conversations.model.UIMessageContent +import com.wire.android.util.ui.UIText +import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.data.message.Message +import com.wire.kalium.logic.data.message.MessageContent +import com.wire.kalium.logic.data.user.User +import com.wire.kalium.logic.data.user.UserAvailabilityStatus +import com.wire.kalium.logic.data.user.UserId +import com.wire.kalium.logic.feature.conversation.ObserveUserListByIdUseCase +import com.wire.kalium.logic.feature.message.GetConversationMessagesFromSearchQueryUseCase +import com.wire.kalium.logic.functional.Either +import io.mockk.MockKAnnotations +import io.mockk.coEvery +import io.mockk.every +import io.mockk.impl.annotations.MockK +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import org.amshove.kluent.internal.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import java.util.UUID + +@OptIn(ExperimentalCoroutinesApi::class) +@ExtendWith(CoroutineTestExtension::class) +class GetConversationMessagesFromSearchUseCaseTest { + + @Test + fun `given below minimum characters to search, when searching, then return an empty list`() = + runTest { + // given + val (arrangement, useCase) = Arrangement() + .arrange() + + // when + val result = useCase("a", arrangement.conversationId) + + // then + assert(result is Either.Right>) + assertEquals( + Either.Right(listOf()), + result + ) + } + + @Test + fun `given search term, when searching messages, then return messages list`() = runTest { + // given + val (arrangement, useCase) = Arrangement() + .withSearchSuccess() + .withMemberIdList() + .withMemberDetails() + .withMappedMessage( + user = Arrangement.user1, + message = Arrangement.message1 + ) + .withMappedMessage( + user = Arrangement.user2, + message = Arrangement.message2 + ) + .arrange() + + // when + val result = useCase(arrangement.searchTerm, arrangement.conversationId) + + // then + assert(result is Either.Right>) + assertEquals( + Arrangement.messages.size, + (result as Either.Right).value.size + ) + } + + class Arrangement { + val searchTerm = "message" + val conversationId = ConversationId( + value = "some-dummy-value", + domain = "some-dummy-domain" + ) + + @MockK + lateinit var getConversationMessagesFromSearch: GetConversationMessagesFromSearchQueryUseCase + + @MockK + lateinit var observeMemberDetailsByIds: ObserveUserListByIdUseCase + + @MockK + lateinit var messageMapper: MessageMapper + + private val useCase: GetConversationMessagesFromSearchUseCase by lazy { + GetConversationMessagesFromSearchUseCase( + getConversationMessagesFromSearch, + observeMemberDetailsByIds, + messageMapper + ) + } + + init { + MockKAnnotations.init(this, relaxUnitFun = true) + } + + suspend fun withSearchSuccess() = apply { + coEvery { + getConversationMessagesFromSearch(searchTerm, conversationId) + } returns Either.Right(messages) + } + + fun withMemberIdList() = apply { + every { messageMapper.memberIdList(messages) } returns listOf( + message1.senderUserId, + message2.senderUserId + ) + } + + suspend fun withMemberDetails() = apply { + coEvery { observeMemberDetailsByIds(any()) } returns flowOf( + listOf(user1, user2) + ) + } + + fun withMappedMessage(user: User, message: Message.Standalone) = apply { + every { messageMapper.toUIMessage(users, message) } returns UIMessage.Regular( + userAvatarData = UserAvatarData( + asset = null, + availabilityStatus = UserAvailabilityStatus.NONE + ), + source = MessageSource.OtherUser, + header = TestMessage.UI_MESSAGE_HEADER.copy( + messageId = UUID.randomUUID().toString(), + userId = user.id + ), + messageContent = UIMessageContent.TextMessage( + MessageBody( + UIText.DynamicString( + (message.content as MessageContent.Text).value + ) + ) + ), + messageFooter = com.wire.android.ui.home.conversations.model.MessageFooter( + TestMessage.UI_MESSAGE_HEADER.messageId + ) + ) + } + + fun arrange() = this to useCase + + companion object { + val user1 = TestUser.OTHER_USER.copy( + id = UserId("user-id1", "domain") + ) + val user2 = TestUser.OTHER_USER.copy( + id = UserId("user-id2", "domain") + ) + val users = listOf(user1, user2) + + val message1 = TestMessage.TEXT_MESSAGE.copy( + content = MessageContent.Text("message1"), + senderUserId = user1.id + ) + val message2 = TestMessage.TEXT_MESSAGE.copy( + content = MessageContent.Text("message2"), + senderUserId = user2.id + ) + val messages = listOf(message1, message2) + } + } +} diff --git a/app/src/test/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreenViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreenViewModelTest.kt index a7c48a3c747..b2563e5e7f7 100644 --- a/app/src/test/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreenViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreenViewModelTest.kt @@ -194,7 +194,7 @@ class OtherUserProfileScreenViewModelTest { @Test fun `given not connected user, then direct conversation is not found`() = runTest { // given - val (arrangement, viewModel) = OtherUserProfileViewModelArrangement() + val (_, viewModel) = OtherUserProfileViewModelArrangement() .withUserInfo( GetUserInfoResult.Success(OTHER_USER.copy(connectionStatus = ConnectionState.NOT_CONNECTED), TEAM) ) diff --git a/kalium b/kalium index c82d8aa7757..2e787e61d0d 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit c82d8aa7757ddb2b22c6ded6120daed6e2c35a2a +Subproject commit 2e787e61d0d1efff61f851952c92e66fb949cd12