From b74015d88313509698552b5ddd0af949dd159388 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20G=C3=B6ransson?= Date: Tue, 23 Jul 2024 11:59:42 +0200 Subject: [PATCH] horror show --- .../relaylist/RelayListExtensions.kt | 153 +++++------------- .../net/mullvad/mullvadvpn/util/FlowUtils.kt | 36 ++++- .../viewmodel/SelectLocationViewModel.kt | 123 +++++++------- 3 files changed, 138 insertions(+), 174 deletions(-) diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayListExtensions.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayListExtensions.kt index d0ae284d865f..f89378e3b80e 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayListExtensions.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayListExtensions.kt @@ -1,5 +1,6 @@ package net.mullvad.mullvadvpn.relaylist +import co.touchlab.kermit.Logger import net.mullvad.mullvadvpn.lib.model.GeoLocationId import net.mullvad.mullvadvpn.lib.model.RelayItem @@ -9,123 +10,43 @@ fun List.findByGeoLocationId(geoLocationId: GeoLocat fun List.findByGeoLocationId(geoLocationId: GeoLocationId.City) = flatMap { it.cities }.firstOrNull { it.id == geoLocationId } -/** - * Filter and expand the list based on search terms If a country is matched, that country and all - * its children are added to the list, but the country is not expanded If a city is matched, its - * parent country is added and expanded if needed and its children are added, but the city is not - * expanded If a relay is matched, its parents are added and expanded and itself is also added. - */ -@Suppress("NestedBlockDepth") -// fun List.filterOnSearchTerm( -// searchTerm: String, -// selectedItem: RelayItemId? -// ): List { -// return if (searchTerm.length >= MIN_SEARCH_LENGTH) { -// val filteredCountries = mutableMapOf() -// this.forEach { relayCountry -> -// val cities = mutableListOf() -// -// // Try to match the search term with a country -// // If we match a country, add that country and all cities and relays in that country -// // Do not currently expand the country or any city -// if (relayCountry.name.contains(other = searchTerm, ignoreCase = true)) { -// cities.addAll(relayCountry.cities.map { city -> city.copy(expanded = false) }) -// filteredCountries[relayCountry.id] = -// relayCountry.copy(expanded = false, cities = cities) -// } -// -// // Go through and try to match the search term with every city -// relayCountry.cities.forEach { relayCity -> -// val relays = mutableListOf() -// // If we match and we already added the country to the filtered list just expand -// the -// // country. -// // If the country is not currently in the filtered list, add it and expand it. -// // Finally if the city has not already been added to the filtered list, add it, -// but -// // do not expand it yet. -// if (relayCity.name.contains(other = searchTerm, ignoreCase = true)) { -// val value = filteredCountries[relayCountry.id] -// if (value != null) { -// filteredCountries[relayCountry.id] = value.copy(expanded = true) -// } else { -// filteredCountries[relayCountry.id] = -// relayCountry.copy(expanded = true, cities = cities) -// } -// if (cities.none { city -> city.id == relayCity.id }) { -// cities.add(relayCity.copy(expanded = false)) -// } -// } -// -// // Go through and try to match the search term with every relay -// relayCity.relays.forEach { relay -> -// // If we match a relay, check if the county the relay is in already is added, -// if -// // so expand, if not add and expand the country. -// // Check if the city that the relay is in is already added to the filtered -// list, -// // if so expand it, if not add it to the filtered list and expand it. -// // Finally add the relay to the list. -// if (relay.name.contains(other = searchTerm, ignoreCase = true)) { -// val value = filteredCountries[relayCountry.id] -// if (value != null) { -// filteredCountries[relayCountry.id] = value.copy(expanded = true) -// } else { -// filteredCountries[relayCountry.id] = -// relayCountry.copy(expanded = true, cities = cities) -// } -// val cityIndex = cities.indexOfFirst { it.id == relayCity.id } -// -// // No city found -// if (cityIndex < 0) { -// cities.add(relayCity.copy(expanded = true, relays = relays)) -// } else { -// // Update found city as expanded -// cities[cityIndex] = cities[cityIndex].copy(expanded = true) -// } -// -// relays.add(relay.copy()) -// } -// } -// } -// } -// filteredCountries.values.sortedBy { it.name } -// } else { -// this.expandItemForSelection(selectedItem) -// } -// } +fun List.newFilterOnSearch(searchTerm: String): Pair, List> { + val matchesIds = + withDescendants().filter { it.name.contains(searchTerm, ignoreCase = true) }.map { it.id } -/// ** Expand the parent(s), if any, for the current selected item */ -// private fun List.expandItemForSelection( -// selectedItem: RelayItemId? -// ): List { -// selectedItem ?: return this -// return when (selectedItem) { -// is CustomListId, -// is GeoLocationId.Country -> this -// is GeoLocationId.City -> -// map { if (it.id == selectedItem.country) it.copy(expanded = true) else it } -// is GeoLocationId.Hostname -> { -// map { country -> -// if (country.id == selectedItem.country) { -// country.copy( -// expanded = true, -// cities = -// country.cities.map { city -> -// if (city.id == selectedItem.city) { -// city.copy(expanded = true) -// } else { -// city -// } -// }, -// ) -// } else { -// country -// } -// } -// } -// } -// } + val expansionSet = matchesIds.flatMap { it.parents() }.toSet() + Logger.d("Expansion Set: $expansionSet") + + val filteredCountryList = filter { it.id in expansionSet || it.id in matchesIds } + .map { + it.copy( + cities = + it.cities + .filter { + it.id in expansionSet || + it.id in matchesIds || + it.id.country in matchesIds + } + .map { + it.copy( + relays = + it.relays.filter { + it.id in expansionSet || + it.id in matchesIds || + it.id.city in matchesIds || + it.id.country in matchesIds + }) + }) + } + return expansionSet to filteredCountryList +} + +private fun GeoLocationId.parents(): List = + when (this) { + is GeoLocationId.City -> listOf(country) + is GeoLocationId.Country -> emptyList() + is GeoLocationId.Hostname -> listOf(country, city) + } fun List.getRelayItemsByCodes( codes: List diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/FlowUtils.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/FlowUtils.kt index 13561aa7f858..5e195252811d 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/FlowUtils.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/FlowUtils.kt @@ -48,8 +48,7 @@ inline fun combine( args[3] as T4, args[4] as T5, args[5] as T6, - args[6] as T7 - ) + args[6] as T7) } } @@ -75,11 +74,40 @@ inline fun combine( args[4] as T5, args[5] as T6, args[6] as T7, - args[7] as T8 - ) + args[7] as T8) } } +inline fun combine( + flow: Flow, + flow2: Flow, + flow3: Flow, + flow4: Flow, + flow5: Flow, + flow6: Flow, + flow7: Flow, + flow8: Flow, + flow9: Flow, + flow10: Flow, + crossinline transform: suspend (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10) -> R +): Flow { + return kotlinx.coroutines.flow.combine( + flow, flow2, flow3, flow4, flow5, flow6, flow7, flow8, flow9, flow10) { args: Array<*> -> + @Suppress("UNCHECKED_CAST") + transform( + args[0] as T1, + args[1] as T2, + args[2] as T3, + args[3] as T4, + args[4] as T5, + args[5] as T6, + args[6] as T7, + args[7] as T8, + args[8] as T9, + args[9] as T10) + } +} + fun Deferred.getOrDefault(default: T) = try { getCompleted() diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SelectLocationViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SelectLocationViewModel.kt index a7af563ab5bb..5fa777ab5644 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SelectLocationViewModel.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SelectLocationViewModel.kt @@ -25,9 +25,11 @@ import net.mullvad.mullvadvpn.lib.model.Provider import net.mullvad.mullvadvpn.lib.model.Providers import net.mullvad.mullvadvpn.lib.model.RelayItem import net.mullvad.mullvadvpn.lib.model.RelayItemId +import net.mullvad.mullvadvpn.relaylist.MIN_SEARCH_LENGTH import net.mullvad.mullvadvpn.relaylist.descendants import net.mullvad.mullvadvpn.relaylist.filterOnOwnershipAndProvider import net.mullvad.mullvadvpn.relaylist.filterOnSearchTerm +import net.mullvad.mullvadvpn.relaylist.newFilterOnSearch import net.mullvad.mullvadvpn.repository.CustomListsRepository import net.mullvad.mullvadvpn.repository.RelayListFilterRepository import net.mullvad.mullvadvpn.repository.RelayListRepository @@ -60,6 +62,8 @@ class SelectLocationViewModel( } private val _expandedItems = MutableStateFlow(initialExpand()) + private val _searchExpandedItems = MutableStateFlow(setOf()) + private val _searchCollapsedItems = MutableStateFlow(setOf()) @Suppress("DestructuringDeclarationWithTooManyEntries") val uiState = @@ -68,6 +72,8 @@ class SelectLocationViewModel( customListsRelayItemUseCase(), relayListRepository.selectedLocation, _expandedItems, + _searchExpandedItems, + _searchCollapsedItems, _searchTerm, relayListFilterRepository.selectedOwnership, availableProvidersUseCase(), @@ -77,6 +83,8 @@ class SelectLocationViewModel( customLists, selectedItem, expandedItems, + searchExpandedItems, + searchCollapsedItems, searchTerm, selectedOwnership, allProviders, @@ -94,10 +102,16 @@ class SelectLocationViewModel( .size } - // val filteredRelayCountries = - // relayCountries.filterOnSearchTerm(searchTerm, - // selectRelayItemId) - // val filteredRelayCountries = relayCountries + val isSearching = searchTerm.length >= MIN_SEARCH_LENGTH + val (expansionList, relayCountries1) = + if (isSearching) { + val (exp, filteredRelayCountries) = + relayCountries.newFilterOnSearch(searchTerm) + (searchExpandedItems + exp.map { it.expandKey() } - searchCollapsedItems) to + filteredRelayCountries + } else { + expandedItems to relayCountries + } val filteredCustomLists = customLists @@ -113,11 +127,11 @@ class SelectLocationViewModel( selectedProvidersCount = selectedProvidersCount, relayListItems = createRelayListItems( + isSearching, selectedItem.getOrNull(), filteredCustomLists, - relayCountries, - expandedItems - ), + relayCountries1, + expansionList), customLists = customLists, selectedItem = selectRelayItemId, ) @@ -132,6 +146,7 @@ class SelectLocationViewModel( val uiSideEffect = _uiSideEffect.receiveAsFlow() fun createRelayListItems( + isSearching: Boolean, selectedItem: RelayItemId?, customLists: List, countries: List, @@ -139,44 +154,37 @@ class SelectLocationViewModel( ): List { val customListItems: List = - customLists.flatMap { customList -> - val expanded = customList.id.expandKey() in expandedkeys - val item = - listOf( - RelayListItem.CustomListItem( - customList, - isSelected = selectedItem == customList.id, - expanded - ) - ) - - if (expanded) { - item + - customList.locations.flatMap { - createCustomListEntry( - parent = customList.id, - item = it, - 1, - expandedkeys - ) + if (isSearching && customLists.isEmpty()) emptyList() + else { + listOf(CustomListHeader) + + customLists.flatMap { customList -> + val expanded = customList.id.expandKey() in expandedkeys + val item = + listOf( + RelayListItem.CustomListItem( + customList, + isSelected = selectedItem == customList.id, + expanded)) + + if (expanded) { + item + + customList.locations.flatMap { + createCustomListEntry( + parent = customList.id, item = it, 1, expandedkeys) + } + } else { + item } - } else { - item - } + } } - val relayLocations: List = countries.flatMap { country -> createGeoLocationEntry(country, selectedItem, expandedkeys = expandedkeys) } - return listOf(CustomListHeader) + - customListItems + - listOf( - RelayListItem.CustomListFooter(customListItems.isNotEmpty()), - RelayListItem.LocationHeader - ) + - relayLocations + return customListItems + listOf( + RelayListItem.CustomListFooter(customListItems.isNotEmpty()), + RelayListItem.LocationHeader) + relayLocations } fun createCustomListEntry( @@ -189,12 +197,7 @@ class SelectLocationViewModel( val entry = listOf( RelayListItem.CustomListEntryItem( - parentId = parent, - item = item, - expanded = expanded, - depth - ) - ) + parentId = parent, item = item, expanded = expanded, depth)) return if (expanded) { entry + @@ -228,8 +231,7 @@ class SelectLocationViewModel( isSelected = selectedItem == item.id, depth = depth, expanded = expanded, - ) - ) + )) return if (expanded) { entry + @@ -271,18 +273,33 @@ class SelectLocationViewModel( } fun onToggleExpand(item: RelayItemId, parent: CustomListId? = null, expand: Boolean) { - _expandedItems.update { + if (_searchTerm.value.length >= MIN_SEARCH_LENGTH) { val key = item.expandKey(parent) if (expand) { - it + key + _searchExpandedItems.update { it + key } + _searchCollapsedItems.update { it - key } } else { - it - key + _searchExpandedItems.update { it - key } + _searchCollapsedItems.update { it + key } + } + } else { + _expandedItems.update { + val key = item.expandKey(parent) + if (expand) { + it + key + } else { + it - key + } } } } fun onSearchTermInput(searchTerm: String) { - viewModelScope.launch { _searchTerm.emit(searchTerm) } + viewModelScope.launch { + _searchExpandedItems.update { setOf() } + _searchCollapsedItems.update { setOf() } + _searchTerm.emit(searchTerm) + } } private fun filterSelectedProvidersByOwnership( @@ -325,14 +342,12 @@ class SelectLocationViewModel( val newLocations = (customList.locations - item.id) customListActionUseCase( - CustomListAction.UpdateLocations(customList.id, newLocations) - ) + CustomListAction.UpdateLocations(customList.id, newLocations)) .bind() } .fold( { SelectLocationSideEffect.GenericError }, - { SelectLocationSideEffect.LocationRemovedFromCustomList(it) } - ) + { SelectLocationSideEffect.LocationRemovedFromCustomList(it) }) _uiSideEffect.send(result) } }