diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchAllPeopleScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchAllPeopleScreen.kt index b58d7f12a50..12b62f37885 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchAllPeopleScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchAllPeopleScreen.kt @@ -16,16 +16,15 @@ * along with this program. If not, see http://www.gnu.org/licenses/. */ +@file:Suppress("TooManyFunctions") + package com.wire.android.ui.home.conversations.search -import androidx.annotation.StringRes import androidx.compose.animation.AnimatedContent -import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.lazy.LazyColumn @@ -36,24 +35,28 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp import com.wire.android.R import com.wire.android.model.Clickable import com.wire.android.model.ItemActionType import com.wire.android.ui.common.button.WireSecondaryButton import com.wire.android.ui.common.dimensions -import com.wire.android.ui.common.progress.WireCircularProgressIndicator +import com.wire.android.ui.common.progress.CenteredCircularProgressBarIndicator import com.wire.android.ui.home.conversations.search.widget.SearchFailureBox +import com.wire.android.ui.home.conversationslist.model.Membership import com.wire.android.ui.home.newconversation.model.Contact import com.wire.android.ui.theme.WireTheme import com.wire.android.util.extension.folderWithElements import com.wire.android.util.ui.PreviewMultipleThemes +import com.wire.kalium.logic.data.user.ConnectionState import com.wire.kalium.logic.data.user.UserId import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableSet +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.persistentSetOf +import kotlinx.collections.immutable.toPersistentList +import kotlinx.collections.immutable.toPersistentSet private const val DEFAULT_SEARCH_RESULT_ITEM_SIZE = 4 @@ -70,10 +73,15 @@ fun SearchAllPeopleScreen( onOpenUserProfile: (Contact) -> Unit, lazyListState: LazyListState = rememberLazyListState() ) { - if (contactsSearchResult.isEmpty() && publicSearchResult.isEmpty()) { - EmptySearchQueryScreen() - } else { - SearchResult( + val emptyResults = contactsSearchResult.isEmpty() && publicSearchResult.isEmpty() + when { + isLoading -> CenteredCircularProgressBarIndicator() + + searchQuery.isBlank() && emptyResults -> EmptySearchQueryScreen() + + searchQuery.isNotBlank() && emptyResults -> SearchFailureBox(R.string.label_no_results_found) + + else -> SearchResult( searchQuery = searchQuery, publicSearchResult = publicSearchResult, contactsSearchResult = contactsSearchResult, @@ -82,7 +90,6 @@ fun SearchAllPeopleScreen( onOpenUserProfile = onOpenUserProfile, lazyListState = lazyListState, isSearchActive = isSearchActive, - isLoading = isLoading, actionType = actionType, ) } @@ -93,7 +100,6 @@ private fun SearchResult( searchQuery: String, contactsSearchResult: ImmutableList, publicSearchResult: ImmutableList, - isLoading: Boolean, isSearchActive: Boolean, actionType: ItemActionType, contactsAddedToGroup: ImmutableSet, @@ -117,8 +123,7 @@ private fun SearchResult( searchQuery = searchQuery, contactsAddedToGroup = contactsAddedToGroup, onChecked = onChecked, - isLoading = isLoading, - contactSearchResult = contactsSearchResult, + searchResult = contactsSearchResult, allItemsVisible = !isSearchActive || searchPeopleScreenState.contactsAllResultsCollapsed, showMoreOrLessButtonVisible = isSearchActive, onShowAllButtonClicked = searchPeopleScreenState::toggleShowAllContactsResult, @@ -131,8 +136,7 @@ private fun SearchResult( externalSearchResults( searchTitle = context.getString(R.string.label_public_wire), searchQuery = searchQuery, - contactSearchResult = publicSearchResult, - isLoading = isLoading, + searchResult = publicSearchResult, allItemsVisible = searchPeopleScreenState.publicResultsCollapsed, showMoreOrLessButtonVisible = isSearchActive, onShowAllButtonClicked = searchPeopleScreenState::toggleShowAllPublicResult, @@ -145,72 +149,6 @@ private fun SearchResult( @Suppress("LongParameterList") private fun LazyListScope.internalSearchResults( - searchTitle: String, - searchQuery: String, - contactsAddedToGroup: ImmutableSet, - onChecked: (Boolean, Contact) -> Unit, - actionType: ItemActionType, - isLoading: Boolean, - contactSearchResult: ImmutableList, - allItemsVisible: Boolean, - showMoreOrLessButtonVisible: Boolean, - onShowAllButtonClicked: () -> Unit, - onOpenUserProfile: (Contact) -> Unit -) { - when { - isLoading -> { - inProgressItem() - } - - else -> { - internalSuccessItem( - searchTitle = searchTitle, - allItemsVisible = allItemsVisible, - showMoreOrLessButtonVisible = showMoreOrLessButtonVisible, - contactsAddedToGroup = contactsAddedToGroup, - onChecked = onChecked, - searchResult = contactSearchResult, - searchQuery = searchQuery, - onShowAllButtonClicked = onShowAllButtonClicked, - onOpenUserProfile = onOpenUserProfile, - actionType = actionType, - ) - } - } -} - -@Suppress("LongParameterList") -private fun LazyListScope.externalSearchResults( - searchTitle: String, - searchQuery: String, - contactSearchResult: ImmutableList, - isLoading: Boolean, - allItemsVisible: Boolean, - showMoreOrLessButtonVisible: Boolean, - onShowAllButtonClicked: () -> Unit, - onOpenUserProfile: (Contact) -> Unit, -) { - when { - isLoading -> { - inProgressItem() - } - - else -> { - externalSuccessItem( - searchTitle = searchTitle, - allItemsVisible = allItemsVisible, - showMoreOrLessButtonVisible = showMoreOrLessButtonVisible, - searchResult = contactSearchResult, - searchQuery = searchQuery, - onShowAllButtonClicked = onShowAllButtonClicked, - onOpenUserProfile = onOpenUserProfile, - ) - } - } -} - -@Suppress("LongParameterList") -private fun LazyListScope.internalSuccessItem( searchTitle: String, allItemsVisible: Boolean, showMoreOrLessButtonVisible: Boolean, @@ -267,7 +205,7 @@ private fun LazyListScope.internalSuccessItem( } @Suppress("LongParameterList") -private fun LazyListScope.externalSuccessItem( +private fun LazyListScope.externalSearchResults( searchTitle: String, allItemsVisible: Boolean, showMoreOrLessButtonVisible: Boolean, @@ -316,30 +254,6 @@ private fun LazyListScope.externalSuccessItem( } } -fun LazyListScope.inProgressItem() { - item { - Box( - Modifier - .fillMaxWidth() - .height(224.dp) - ) { - WireCircularProgressIndicator( - progressColor = Color.Black, - modifier = Modifier.align( - Alignment.Center - ) - ) - } - } -} - -fun LazyListScope.failureItem(@StringRes failureMessage: Int) { - item { - SearchFailureBox(failureMessage) - } -} - -@OptIn(ExperimentalAnimationApi::class) @Composable private fun ShowButton( isShownAll: Boolean, @@ -366,3 +280,116 @@ fun PreviewShowButton() { ShowButton(isShownAll = false, onShowButtonClicked = {}) } } + +@PreviewMultipleThemes +@Composable +fun PreviewSearchAllPeopleScreen_Loading() = WireTheme { + SearchAllPeopleScreen( + searchQuery = "", + contactsSearchResult = persistentListOf(), + publicSearchResult = persistentListOf(), + contactsAddedToGroup = persistentSetOf(), + isLoading = true, + isSearchActive = false, + actionType = ItemActionType.CHECK, + onChecked = { _, _ -> }, + onOpenUserProfile = {} + ) +} + +@PreviewMultipleThemes +@Composable +fun PreviewSearchAllPeopleScreen_InitialResults() = WireTheme { + val contacts = previewContactsList(count = 10, startIndex = 0, isContact = true).toPersistentList() + SearchAllPeopleScreen( + searchQuery = "", + contactsSearchResult = contacts, + publicSearchResult = persistentListOf(), + contactsAddedToGroup = persistentSetOf(), + isLoading = false, + isSearchActive = false, + actionType = ItemActionType.CHECK, + onChecked = { _, _ -> }, + onOpenUserProfile = {} + ) +} + +@PreviewMultipleThemes +@Composable +fun PreviewSearchAllPeopleScreen_EmptyInitialResults() = WireTheme { + SearchAllPeopleScreen( + searchQuery = "", + contactsSearchResult = persistentListOf(), + publicSearchResult = persistentListOf(), + contactsAddedToGroup = persistentSetOf(), + isLoading = false, + isSearchActive = false, + actionType = ItemActionType.CHECK, + onChecked = { _, _ -> }, + onOpenUserProfile = {} + ) +} + +@PreviewMultipleThemes +@Composable +fun PreviewSearchAllPeopleScreen_SearchResults_TypeClick() = WireTheme { + val contacts = previewContactsList(count = 10, startIndex = 0, isContact = true).toPersistentList() + val public = previewContactsList(count = 10, startIndex = 10, isContact = false).toPersistentList() + SearchAllPeopleScreen( + searchQuery = "Con", + contactsSearchResult = contacts, + publicSearchResult = public, + contactsAddedToGroup = persistentSetOf(), + isLoading = false, + isSearchActive = true, + actionType = ItemActionType.CLICK, + onChecked = { _, _ -> }, + onOpenUserProfile = {} + ) +} + +@PreviewMultipleThemes +@Composable +fun PreviewSearchAllPeopleScreen_SearchResults_TypeCheck() = WireTheme { + val contacts = previewContactsList(count = 10, startIndex = 0, isContact = true).toPersistentList() + val public = previewContactsList(count = 10, startIndex = 10, isContact = false).toPersistentList() + val selectedContacts = contacts.filterIndexed { index, _ -> index % 3 == 0 }.toPersistentSet() + SearchAllPeopleScreen( + searchQuery = "Con", + contactsSearchResult = contacts, + publicSearchResult = public, + contactsAddedToGroup = selectedContacts, + isLoading = false, + isSearchActive = true, + actionType = ItemActionType.CHECK, + onChecked = { _, _ -> }, + onOpenUserProfile = {} + ) +} + +@PreviewMultipleThemes +@Composable +fun PreviewSearchAllPeopleScreen_EmptySearchResults() = WireTheme { + SearchAllPeopleScreen( + searchQuery = "Con", + contactsSearchResult = persistentListOf(), + publicSearchResult = persistentListOf(), + contactsAddedToGroup = persistentSetOf(), + isLoading = false, + isSearchActive = true, + actionType = ItemActionType.CLICK, + onChecked = { _, _ -> }, + onOpenUserProfile = {} + ) +} + +private fun previewContact(index: Int, isContact: Boolean) = Contact( + id = index.toString(), + domain = "wire.com", + name = "Contact nr $index", + connectionState = if (isContact) ConnectionState.ACCEPTED else ConnectionState.NOT_CONNECTED, + membership = Membership.Standard, +) + +private fun previewContactsList(count: Int, startIndex: Int = 0, isContact: Boolean): List = + buildList { repeat(count) { index -> add(previewContact(startIndex + index, isContact)) } } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchAllServicesScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchAllServicesScreen.kt index 50885b115a9..2f5f911cc11 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchAllServicesScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchAllServicesScreen.kt @@ -38,9 +38,15 @@ import com.wire.android.ui.common.UserProfileAvatar import com.wire.android.ui.common.dimensions import com.wire.android.ui.common.progress.CenteredCircularProgressBarIndicator import com.wire.android.ui.home.conversations.search.widget.SearchFailureBox +import com.wire.android.ui.home.conversationslist.model.Membership import com.wire.android.ui.home.newconversation.model.Contact +import com.wire.android.ui.theme.WireTheme import com.wire.android.util.extension.folderWithElements +import com.wire.android.util.ui.PreviewMultipleThemes +import com.wire.kalium.logic.data.user.ConnectionState import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList @Composable fun SearchAllServicesScreen( @@ -58,7 +64,6 @@ fun SearchAllServicesScreen( onServiceClicked = onServiceClicked, result = searchServicesViewModel.state.result, lazyListState = lazyListState, - error = searchServicesViewModel.state.error, isLoading = searchServicesViewModel.state.isLoading ) } @@ -68,27 +73,23 @@ private fun SearchAllServicesContent( searchQuery: String, result: ImmutableList, isLoading: Boolean, - error: Boolean, onServiceClicked: (Contact) -> Unit, lazyListState: LazyListState = rememberLazyListState() ) { when { isLoading -> CenteredCircularProgressBarIndicator() - error -> SearchFailureBox(failureMessage = R.string.label_general_error) // TODO(user experience): what to do when user team has no services? - result.isEmpty() -> { - EmptySearchQueryScreen() - } + searchQuery.isBlank() && result.isEmpty() -> EmptySearchQueryScreen() - else -> { - SuccessServicesList( - searchQuery = searchQuery, - onServiceClicked = onServiceClicked, - services = result, - lazyListState = lazyListState - ) - } + searchQuery.isNotBlank() && result.isEmpty() -> SearchFailureBox(R.string.label_no_results_found) + + else -> SuccessServicesList( + searchQuery = searchQuery, + onServiceClicked = onServiceClicked, + services = result, + lazyListState = lazyListState + ) } } @@ -139,3 +140,45 @@ private fun SuccessServicesList( } } } + +@PreviewMultipleThemes +@Composable +fun PreviewSearchAllServicesScreen_Loading() = WireTheme { + SearchAllServicesContent("", persistentListOf(), true, {}) +} + +@PreviewMultipleThemes +@Composable +fun PreviewSearchAllServicesScreen_InitialResults() = WireTheme { + SearchAllServicesContent("", previewServiceList(count = 10).toPersistentList(), false, {}) +} + +@PreviewMultipleThemes +@Composable +fun PreviewSearchAllServicesScreen_EmptyInitialResults() = WireTheme { + SearchAllServicesContent("", persistentListOf(), false, {}) +} + +@PreviewMultipleThemes +@Composable +fun PreviewSearchAllServicesScreen_SearchResults() = WireTheme { + SearchAllServicesContent("Serv", previewServiceList(count = 10).toPersistentList(), false, {}) +} + +@PreviewMultipleThemes +@Composable +fun PreviewSearchAllServicesScreen_EmptySearchResults() = WireTheme { + SearchAllServicesContent("Serv", persistentListOf(), false, {}) +} + +private fun previewService(index: Int) = Contact( + id = index.toString(), + domain = "wire.com", + name = "Service nr $index", + connectionState = ConnectionState.NOT_CONNECTED, + membership = Membership.Service, +) + +private fun previewServiceList(count: Int): List = buildList { + repeat(count) { index -> add(previewService(index)) } +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchPeopleRouter.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchPeopleRouter.kt index 408049526de..87559beafd4 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchPeopleRouter.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchPeopleRouter.kt @@ -155,7 +155,6 @@ fun SearchUsersAndServicesScreen( onOpenUserProfile = onOpenUserProfile, onContactChecked = onContactChecked, isSearchActive = isSearchActive, - isLoading = false, // TODO: update correctly actionType = actionType, ) } @@ -176,7 +175,6 @@ fun SearchUsersAndServicesScreen( onContactChecked = onContactChecked, onOpenUserProfile = onOpenUserProfile, isSearchActive = isSearchActive, - isLoading = false, // TODO: update correctly actionType = actionType, ) } @@ -236,7 +234,6 @@ enum class SearchPeopleScreenType { private fun SearchAllPeopleOrContactsScreen( searchQuery: String, contactsAddedToGroup: ImmutableSet, - isLoading: Boolean, isSearchActive: Boolean, actionType: ItemActionType, onOpenUserProfile: (Contact) -> Unit, @@ -258,7 +255,7 @@ private fun SearchAllPeopleOrContactsScreen( onOpenUserProfile = onOpenUserProfile, lazyListState = lazyState, isSearchActive = isSearchActive, - isLoading = isLoading, + isLoading = searchUserViewModel.state.isLoading, actionType = actionType, ) } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchServicesViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchServicesViewModel.kt index 31aef8992f5..16b345f61ad 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchServicesViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchServicesViewModel.kt @@ -46,7 +46,7 @@ class SearchServicesViewModel @Inject constructor( private val searchServicesByName: SearchServicesByNameUseCase, ) : ViewModel() { private val searchQueryTextFlow = MutableStateFlow(String.EMPTY) - var state: SearchServicesState by mutableStateOf(SearchServicesState()) + var state: SearchServicesState by mutableStateOf(SearchServicesState(isLoading = true)) private set init { @@ -68,15 +68,12 @@ class SearchServicesViewModel @Inject constructor( private fun search(query: String) { viewModelScope.launch { - if (query.isEmpty()) { - getAllServices().first().also { services -> - state = state.copy(result = services.map(contactMapper::fromService).toImmutableList(), searchQuery = query) - } + val result = if (query.isEmpty()) { + getAllServices().first() } else { - searchServicesByName(query).first().also { services -> - state = state.copy(result = services.map(contactMapper::fromService).toImmutableList(), searchQuery = query) - } + searchServicesByName(query).first() } + state = state.copy(isLoading = false, searchQuery = query, result = result.map(contactMapper::fromService).toImmutableList()) } } } @@ -85,5 +82,4 @@ data class SearchServicesState( val result: ImmutableList = persistentListOf(), val searchQuery: String = String.EMPTY, val isLoading: Boolean = false, - val error: Boolean = false ) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchUserViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchUserViewModel.kt index 9a98d77b348..b409aacda28 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchUserViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchUserViewModel.kt @@ -61,7 +61,7 @@ class SearchUserViewModel @Inject constructor( } private val searchQueryTextFlow = MutableStateFlow(String.EMPTY) - var state: SearchUserState by mutableStateOf(SearchUserState()) + var state: SearchUserState by mutableStateOf(SearchUserState(isLoading = true)) private set init { @@ -99,6 +99,7 @@ class SearchUserViewModel @Inject constructor( customDomain = domain ).also { userSearchEntities -> state = state.copy( + isLoading = false, contactsResult = userSearchEntities.connected.map(contactMapper::fromSearchUserResult).toImmutableList(), publicResult = userSearchEntities.notConnected.map(contactMapper::fromSearchUserResult).toImmutableList(), searchQuery = searchTerm @@ -113,6 +114,7 @@ class SearchUserViewModel @Inject constructor( customDomain = domain ).also { userSearchEntities -> state = state.copy( + isLoading = false, contactsResult = userSearchEntities.connected.map(contactMapper::fromSearchUserResult).toImmutableList(), publicResult = userSearchEntities.notConnected.map(contactMapper::fromSearchUserResult).toImmutableList(), searchQuery = searchTerm @@ -125,4 +127,5 @@ data class SearchUserState( val contactsResult: ImmutableList = persistentListOf(), val publicResult: ImmutableList = persistentListOf(), val searchQuery: String = String.EMPTY, + val isLoading: Boolean = false, ) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/widget/SearchFailureWidget.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/widget/SearchFailureWidget.kt index 3765144738d..fadffceff3c 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/widget/SearchFailureWidget.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/widget/SearchFailureWidget.kt @@ -20,26 +20,21 @@ package com.wire.android.ui.home.conversations.search.widget import androidx.annotation.StringRes import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.fillMaxSize 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.tooling.preview.Preview -import androidx.compose.ui.unit.dp +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 SearchFailureBox(@StringRes failureMessage: Int) { - Box( - Modifier - .fillMaxWidth() - .height(224.dp) - ) { + Box(Modifier.fillMaxSize()) { Text( stringResource(id = failureMessage), modifier = Modifier.align(Alignment.Center), @@ -48,8 +43,8 @@ fun SearchFailureBox(@StringRes failureMessage: Int) { } } -@Preview +@PreviewMultipleThemes @Composable -fun SearchFailureBoxPreview() { +fun SearchFailureBoxPreview() = WireTheme { SearchFailureBox(failureMessage = com.wire.android.R.string.label_no_results_found) }