From 0ae05709db9bdf00cfe6842d788e2d30cda0b314 Mon Sep 17 00:00:00 2001 From: Danil Nikolaev Date: Wed, 26 Mar 2025 01:28:50 +0300 Subject: [PATCH] feat: Add ordering functionality for friends list --- .../data/api/friends/FriendsRepository.kt | 2 + .../data/api/friends/FriendsRepositoryImpl.kt | 6 +- .../dev/meloda/fast/domain/FriendsUseCase.kt | 2 + .../meloda/fast/domain/FriendsUseCaseImpl.kt | 15 +++- .../res/drawable/round_filter_list_24.xml | 11 +++ .../meloda/fast/friends/FriendsViewModel.kt | 89 +++++++++++-------- .../fast/friends/model/FriendsScreenState.kt | 4 +- .../friends/presentation/FriendsScreen.kt | 5 ++ .../friends/presentation/RootFriendsScreen.kt | 62 ++++++++++++- 9 files changed, 149 insertions(+), 47 deletions(-) create mode 100644 core/ui/src/main/res/drawable/round_filter_list_24.xml diff --git a/core/data/src/main/kotlin/dev/meloda/fast/data/api/friends/FriendsRepository.kt b/core/data/src/main/kotlin/dev/meloda/fast/data/api/friends/FriendsRepository.kt index 0f37d866..b0d9d61e 100644 --- a/core/data/src/main/kotlin/dev/meloda/fast/data/api/friends/FriendsRepository.kt +++ b/core/data/src/main/kotlin/dev/meloda/fast/data/api/friends/FriendsRepository.kt @@ -8,11 +8,13 @@ import com.slack.eithernet.ApiResult interface FriendsRepository { suspend fun getAllFriends( + order: String, count: Int?, offset: Int? ): ApiResult suspend fun getFriends( + order: String, count: Int?, offset: Int? ): ApiResult, RestApiErrorDomain> diff --git a/core/data/src/main/kotlin/dev/meloda/fast/data/api/friends/FriendsRepositoryImpl.kt b/core/data/src/main/kotlin/dev/meloda/fast/data/api/friends/FriendsRepositoryImpl.kt index 3723e688..b526bc90 100644 --- a/core/data/src/main/kotlin/dev/meloda/fast/data/api/friends/FriendsRepositoryImpl.kt +++ b/core/data/src/main/kotlin/dev/meloda/fast/data/api/friends/FriendsRepositoryImpl.kt @@ -25,10 +25,11 @@ class FriendsRepositoryImpl( ) : FriendsRepository { override suspend fun getAllFriends( + order: String, count: Int?, offset: Int? ): ApiResult = withContext(Dispatchers.IO) { - val friends = async { getFriends(count, offset) }.await() + val friends = async { getFriends(order, count, offset) }.await() .successOrElse { failure -> return@withContext failure } @@ -42,11 +43,12 @@ class FriendsRepositoryImpl( } override suspend fun getFriends( + order: String, count: Int?, offset: Int? ): ApiResult, RestApiErrorDomain> = withContext(Dispatchers.IO) { val requestModel = GetFriendsRequest( - order = "hints", + order = order, count = count, offset = offset, fields = VkConstants.USER_FIELDS diff --git a/core/domain/src/main/kotlin/dev/meloda/fast/domain/FriendsUseCase.kt b/core/domain/src/main/kotlin/dev/meloda/fast/domain/FriendsUseCase.kt index d70c2acb..67b05e14 100644 --- a/core/domain/src/main/kotlin/dev/meloda/fast/domain/FriendsUseCase.kt +++ b/core/domain/src/main/kotlin/dev/meloda/fast/domain/FriendsUseCase.kt @@ -8,11 +8,13 @@ import kotlinx.coroutines.flow.Flow interface FriendsUseCase { fun getAllFriends( + order: String = "hints", count: Int?, offset: Int? ): Flow> fun getFriends( + order: String = "hints", count: Int?, offset: Int? ): Flow>> diff --git a/core/domain/src/main/kotlin/dev/meloda/fast/domain/FriendsUseCaseImpl.kt b/core/domain/src/main/kotlin/dev/meloda/fast/domain/FriendsUseCaseImpl.kt index 19d22bf5..53c54945 100644 --- a/core/domain/src/main/kotlin/dev/meloda/fast/domain/FriendsUseCaseImpl.kt +++ b/core/domain/src/main/kotlin/dev/meloda/fast/domain/FriendsUseCaseImpl.kt @@ -11,19 +11,26 @@ import kotlinx.coroutines.flow.flow class FriendsUseCaseImpl(private val repository: FriendsRepository) : FriendsUseCase { - override fun getAllFriends(count: Int?, offset: Int?): Flow> = flow { + override fun getAllFriends(order: String, count: Int?, offset: Int?): Flow> = flow { emit(State.Loading) - val newState = repository.getAllFriends(count, offset).mapToState() + val newState = repository.getAllFriends(order, count, offset).mapToState() emit(newState) } override fun getFriends( - count: Int?, offset: Int? + order: String, + count: Int?, + offset: Int? ): Flow>> = flow { emit(State.Loading) - val newState = repository.getFriends(count, offset).mapToState() + val newState = repository.getFriends( + order = order, + count = count, + offset = offset + ).mapToState() + emit(newState) } diff --git a/core/ui/src/main/res/drawable/round_filter_list_24.xml b/core/ui/src/main/res/drawable/round_filter_list_24.xml new file mode 100644 index 00000000..6d8c5ede --- /dev/null +++ b/core/ui/src/main/res/drawable/round_filter_list_24.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/feature/friends/src/main/kotlin/dev/meloda/fast/friends/FriendsViewModel.kt b/feature/friends/src/main/kotlin/dev/meloda/fast/friends/FriendsViewModel.kt index e17b28b6..0a6cdcbb 100644 --- a/feature/friends/src/main/kotlin/dev/meloda/fast/friends/FriendsViewModel.kt +++ b/feature/friends/src/main/kotlin/dev/meloda/fast/friends/FriendsViewModel.kt @@ -34,6 +34,8 @@ interface FriendsViewModel { fun setScrollIndex(index: Int) fun setScrollOffset(offset: Int) + + fun onOrderTypeChanged(newOrderType: String) } abstract class BaseFriendsViewModelImpl : ViewModel(), FriendsViewModel { @@ -69,6 +71,12 @@ abstract class BaseFriendsViewModelImpl : ViewModel(), FriendsViewModel { screenState.setValue { old -> old.copy(scrollOffset = offset) } } + override fun onOrderTypeChanged(newOrderType: String) { + if (screenState.value.orderType == newOrderType) return + screenState.setValue { old -> old.copy(orderType = newOrderType) } + loadFriends(offset = 0) + } + abstract fun loadFriends(offset: Int = currentOffset.value) protected fun handleError(error: State.Error) { @@ -138,52 +146,55 @@ class FriendsViewModelImpl( } override fun loadFriends(offset: Int) { - friendsUseCase.getFriends(count = LOAD_COUNT, offset = offset) - .listenValue(viewModelScope) { state -> - state.processState( - error = ::handleError, - success = { response -> - val itemsCountSufficient = response.size == LOAD_COUNT - canPaginate.setValue { itemsCountSufficient } + friendsUseCase.getFriends( + order = screenState.value.orderType, + count = LOAD_COUNT, + offset = offset + ).listenValue(viewModelScope) { state -> + state.processState( + error = ::handleError, + success = { response -> + val itemsCountSufficient = response.size == LOAD_COUNT + canPaginate.setValue { itemsCountSufficient } - val paginationExhausted = !itemsCountSufficient && - screenState.value.friends.size >= LOAD_COUNT + val paginationExhausted = !itemsCountSufficient && + screenState.value.friends.size >= LOAD_COUNT - imagesToPreload.setValue { - response.mapNotNull(VkUser::photo100) + imagesToPreload.setValue { + response.mapNotNull(VkUser::photo100) + } + + friendsUseCase.storeUsers(response) + + val loadedFriends = response.map { + it.asPresentation(userSettings.useContactNames.value) + } + + val newState = screenState.value.copy( + isPaginationExhausted = paginationExhausted + ) + + if (offset == 0) { + friends.emit(response) + screenState.setValue { + newState.copy(friends = loadedFriends) } - - friendsUseCase.storeUsers(response) - - val loadedFriends = response.map { - it.asPresentation(userSettings.useContactNames.value) - } - - val newState = screenState.value.copy( - isPaginationExhausted = paginationExhausted - ) - - if (offset == 0) { - friends.emit(response) - screenState.setValue { - newState.copy(friends = loadedFriends) - } - } else { - friends.emit(friends.value.plus(response)) - screenState.setValue { - newState.copy(friends = newState.friends.plus(loadedFriends)) - } + } else { + friends.emit(friends.value.plus(response)) + screenState.setValue { + newState.copy(friends = newState.friends.plus(loadedFriends)) } } - ) - - screenState.setValue { old -> - old.copy( - isLoading = offset == 0 && state.isLoading(), - isPaginating = offset > 0 && state.isLoading() - ) } + ) + + screenState.setValue { old -> + old.copy( + isLoading = offset == 0 && state.isLoading(), + isPaginating = offset > 0 && state.isLoading() + ) } + } } } diff --git a/feature/friends/src/main/kotlin/dev/meloda/fast/friends/model/FriendsScreenState.kt b/feature/friends/src/main/kotlin/dev/meloda/fast/friends/model/FriendsScreenState.kt index 4664edfb..967466e4 100644 --- a/feature/friends/src/main/kotlin/dev/meloda/fast/friends/model/FriendsScreenState.kt +++ b/feature/friends/src/main/kotlin/dev/meloda/fast/friends/model/FriendsScreenState.kt @@ -11,6 +11,7 @@ data class FriendsScreenState( val isPaginationExhausted: Boolean, val scrollIndex: Int, val scrollOffset: Int, + val orderType: String, ) { companion object { @@ -20,7 +21,8 @@ data class FriendsScreenState( isPaginating = false, isPaginationExhausted = false, scrollIndex = 0, - scrollOffset = 0 + scrollOffset = 0, + orderType = "hints" ) } } diff --git a/feature/friends/src/main/kotlin/dev/meloda/fast/friends/presentation/FriendsScreen.kt b/feature/friends/src/main/kotlin/dev/meloda/fast/friends/presentation/FriendsScreen.kt index 6e403c8b..85ceed61 100644 --- a/feature/friends/src/main/kotlin/dev/meloda/fast/friends/presentation/FriendsScreen.kt +++ b/feature/friends/src/main/kotlin/dev/meloda/fast/friends/presentation/FriendsScreen.kt @@ -45,6 +45,7 @@ import org.koin.androidx.compose.koinViewModel @Composable fun FriendsScreen( modifier: Modifier = Modifier, + orderType: String, padding: PaddingValues, tabIndex: Int, onSessionExpiredLogOutButtonClicked: () -> Unit = {}, @@ -60,6 +61,10 @@ fun FriendsScreen( koinViewModel() } + LaunchedEffect(orderType) { + viewModel.onOrderTypeChanged(orderType) + } + val screenState by viewModel.screenState.collectAsStateWithLifecycle() val baseError by viewModel.baseError.collectAsStateWithLifecycle() val canPaginate by viewModel.canPaginate.collectAsStateWithLifecycle() diff --git a/feature/friends/src/main/kotlin/dev/meloda/fast/friends/presentation/RootFriendsScreen.kt b/feature/friends/src/main/kotlin/dev/meloda/fast/friends/presentation/RootFriendsScreen.kt index cf4a108c..12a4c515 100644 --- a/feature/friends/src/main/kotlin/dev/meloda/fast/friends/presentation/RootFriendsScreen.kt +++ b/feature/friends/src/main/kotlin/dev/meloda/fast/friends/presentation/RootFriendsScreen.kt @@ -13,6 +13,8 @@ import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.PrimaryTabRow import androidx.compose.material3.Scaffold @@ -32,6 +34,7 @@ import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -40,9 +43,15 @@ import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi import dev.chrisbanes.haze.materials.HazeMaterials import dev.meloda.fast.model.BaseError import dev.meloda.fast.ui.R +import dev.meloda.fast.ui.components.ActionInvokeDismiss +import dev.meloda.fast.ui.components.MaterialDialog +import dev.meloda.fast.ui.components.SelectionType import dev.meloda.fast.ui.model.TabItem import dev.meloda.fast.ui.theme.LocalHazeState import dev.meloda.fast.ui.theme.LocalThemeConfig +import dev.meloda.fast.ui.util.ImmutableList + +import dev.meloda.fast.ui.R as UiR @OptIn(ExperimentalMaterial3Api::class, ExperimentalHazeMaterialsApi::class) @Composable @@ -98,6 +107,44 @@ fun FriendsRoute( ) } + var orderType: String by remember { mutableStateOf("hints") } + + var showOrderDialog by remember { mutableStateOf(false) } + + val orderItems = remember { + mapOf( + "hints" to "Priority", + "name" to "Name", + "random" to "Random", + "mobile" to "Mobile", + "smart" to "Smart" + ) + } + + var selectedIndex by remember { + mutableIntStateOf(0) + } + + if (showOrderDialog) { + MaterialDialog( + onDismissRequest = { showOrderDialog = false }, + confirmText = stringResource(R.string.ok), + confirmAction = { + orderType = + orderItems.keys.toCollection(mutableListOf())[selectedIndex] + }, + cancelText = stringResource(R.string.cancel), + selectionType = SelectionType.Single, + items = ImmutableList.copyOf(orderItems.values), + preSelectedItems = ImmutableList.of(selectedIndex), + onItemClick = { + selectedIndex = it + }, + title = "Order type", + actionInvokeDismiss = ActionInvokeDismiss.Always + ) + } + Scaffold( modifier = Modifier.fillMaxSize(), contentWindowInsets = WindowInsets.statusBars, @@ -129,7 +176,19 @@ fun FriendsRoute( colors = TopAppBarDefaults.topAppBarColors( containerColor = Color.Transparent ), - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxWidth(), + actions = { + IconButton( + onClick = { + showOrderDialog = true + } + ) { + Icon( + painter = painterResource(UiR.drawable.round_filter_list_24), + contentDescription = null + ) + } + } ) PrimaryTabRow( selectedTabIndex = selectedTabIndex, @@ -175,6 +234,7 @@ fun FriendsRoute( modifier = Modifier.fillMaxSize(), ) { index -> FriendsScreen( + orderType = orderType, padding = padding, tabIndex = index, onSessionExpiredLogOutButtonClicked = { onError(BaseError.SessionExpired) },