diff --git a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/ConversationsViewModel.kt b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/ConversationsViewModel.kt index 78cc31d3..f87c309a 100644 --- a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/ConversationsViewModel.kt +++ b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/ConversationsViewModel.kt @@ -60,6 +60,9 @@ interface ConversationsViewModel { fun onOptionClicked(conversation: UiConversation, option: ConversationOption) fun onErrorConsumed() + + fun setScrollIndex(index: Int) + fun setScrollOffset(offset: Int) } class ConversationsViewModelImpl( @@ -206,6 +209,14 @@ class ConversationsViewModelImpl( baseError.setValue { null } } + override fun setScrollIndex(index: Int) { + screenState.setValue { old -> old.copy(scrollIndex = index) } + } + + override fun setScrollOffset(offset: Int) { + screenState.setValue { old -> old.copy(scrollOffset = offset) } + } + private fun hideOptions(conversationId: Int) { screenState.setValue { old -> old.copy( diff --git a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/model/ConversationsScreenState.kt b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/model/ConversationsScreenState.kt index 38e0733e..27277405 100644 --- a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/model/ConversationsScreenState.kt +++ b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/model/ConversationsScreenState.kt @@ -9,7 +9,9 @@ data class ConversationsScreenState( val isLoading: Boolean, val isPaginating: Boolean, val isPaginationExhausted: Boolean, - val profileImageUrl: String? + val profileImageUrl: String?, + val scrollIndex: Int, + val scrollOffset: Int ) { companion object { @@ -19,7 +21,9 @@ data class ConversationsScreenState( isLoading = true, isPaginating = false, isPaginationExhausted = false, - profileImageUrl = null + profileImageUrl = null, + scrollIndex = 0, + scrollOffset = 0, ) } } diff --git a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/presentation/ConversationsList.kt b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/presentation/ConversationsList.kt index 44232433..835a2677 100644 --- a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/presentation/ConversationsList.kt +++ b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/presentation/ConversationsList.kt @@ -45,8 +45,6 @@ fun ConversationsList( ) { val coroutineScope = rememberCoroutineScope() - val conversations = screenState.conversations - val bottomPadding = LocalBottomPadding.current LazyColumn( @@ -55,9 +53,10 @@ fun ConversationsList( ) { item { Spacer(modifier = Modifier.height(padding.calculateTopPadding())) + Spacer(modifier = Modifier.height(8.dp)) } items( - items = conversations, + items = screenState.conversations, key = UiConversation::id, ) { conversation -> val isUserAccount by remember(conversation) { diff --git a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/presentation/ConversationsScreen.kt b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/presentation/ConversationsScreen.kt index 1d79030f..841f19c4 100644 --- a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/presentation/ConversationsScreen.kt +++ b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/presentation/ConversationsScreen.kt @@ -1,6 +1,5 @@ package dev.meloda.fast.conversations.presentation -import android.content.SharedPreferences import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.Animatable @@ -29,6 +28,7 @@ import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.LinearProgressIndicator @@ -63,7 +63,6 @@ import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp -import androidx.core.content.edit import androidx.core.view.HapticFeedbackConstantsCompat import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil.imageLoader @@ -88,7 +87,6 @@ import dev.meloda.fast.ui.util.isScrollingUp import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.launch -import org.koin.compose.koinInject import dev.meloda.fast.ui.R as UiR @Composable @@ -100,8 +98,6 @@ fun ConversationsRoute( ) { val context = LocalContext.current - val prefs: SharedPreferences = koinInject() - val screenState by viewModel.screenState.collectAsStateWithLifecycle() val baseError by viewModel.baseError.collectAsStateWithLifecycle() val canPaginate by viewModel.canPaginate.collectAsStateWithLifecycle() @@ -121,8 +117,6 @@ fun ConversationsRoute( screenState = screenState, baseError = baseError, canPaginate = canPaginate, - firstVisibleItemIndex = prefs.getInt("conversations_all_scroll_position", 0), - firstVisibleItemScrollOffset = prefs.getInt("conversations_all_scroll_offset", 0), onSessionExpiredLogOutButtonClicked = { onError(BaseError.SessionExpired) }, onConversationItemClicked = { id -> onConversationItemClicked(id) @@ -134,15 +128,10 @@ fun ConversationsRoute( onRefreshDropdownItemClicked = viewModel::onRefresh, onRefresh = viewModel::onRefresh, onConversationPhotoClicked = onConversationPhotoClicked, - onSaveScrollPosition = { index -> - prefs.edit { putInt("conversations_all_scroll_position", index) } - }, - onSaveScrollOffsetPosition = { offset -> - prefs.edit { putInt("conversations_all_scroll_offset", offset) } - } + setScrollIndex = viewModel::setScrollIndex, + setScrollOffset = viewModel::setScrollOffset ) - HandleDialogs( screenState = screenState, viewModel = viewModel @@ -158,8 +147,6 @@ fun ConversationsScreen( screenState: ConversationsScreenState = ConversationsScreenState.EMPTY, baseError: BaseError? = null, canPaginate: Boolean = false, - firstVisibleItemIndex: Int = 0, - firstVisibleItemScrollOffset: Int = 0, onSessionExpiredLogOutButtonClicked: () -> Unit, onConversationItemClicked: (conversationId: Int) -> Unit = {}, onConversationItemLongClicked: (conversation: UiConversation) -> Unit = {}, @@ -168,8 +155,8 @@ fun ConversationsScreen( onRefreshDropdownItemClicked: () -> Unit = {}, onRefresh: () -> Unit = {}, onConversationPhotoClicked: (url: String) -> Unit = {}, - onSaveScrollPosition: (Int) -> Unit = {}, - onSaveScrollOffsetPosition: (Int) -> Unit = {} + setScrollIndex: (Int) -> Unit = {}, + setScrollOffset: (Int) -> Unit = {} ) { val view = LocalView.current val currentTheme = LocalThemeConfig.current @@ -179,20 +166,20 @@ fun ConversationsScreen( } val listState = rememberLazyListState( - initialFirstVisibleItemIndex = firstVisibleItemIndex, - initialFirstVisibleItemScrollOffset = firstVisibleItemScrollOffset + initialFirstVisibleItemIndex = screenState.scrollIndex, + initialFirstVisibleItemScrollOffset = screenState.scrollOffset ) LaunchedEffect(listState) { snapshotFlow { listState.firstVisibleItemIndex } .debounce(500L) - .collectLatest(onSaveScrollPosition) + .collectLatest(setScrollIndex) } LaunchedEffect(listState) { snapshotFlow { listState.firstVisibleItemScrollOffset } .debounce(500L) - .collectLatest(onSaveScrollOffsetPosition) + .collectLatest(setScrollOffset) } val paginationConditionMet by remember(canPaginate, listState) { @@ -306,6 +293,9 @@ fun ConversationsScreen( AnimatedVisibility(showHorizontalProgressBar) { LinearProgressIndicator(modifier = Modifier.fillMaxWidth()) } + AnimatedVisibility(!showHorizontalProgressBar) { + HorizontalDivider() + } } }, floatingActionButton = { 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 b3f51389..fee14622 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 @@ -31,6 +31,11 @@ interface FriendsViewModel { fun onRefresh() fun onErrorConsumed() + + fun setScrollIndex(index: Int) + fun setScrollOffset(offset: Int) + fun setScrollIndexOnline(index: Int) + fun setScrollOffsetOnline(offset: Int) } class FriendsViewModelImpl( @@ -66,6 +71,22 @@ class FriendsViewModelImpl( baseError.setValue { null } } + override fun setScrollIndex(index: Int) { + screenState.setValue { old -> old.copy(scrollIndex = index) } + } + + override fun setScrollOffset(offset: Int) { + screenState.setValue { old -> old.copy(scrollOffset = offset) } + } + + override fun setScrollIndexOnline(index: Int) { + screenState.setValue { old -> old.copy(scrollIndexOnline = index) } + } + + override fun setScrollOffsetOnline(offset: Int) { + screenState.setValue { old -> old.copy(scrollOffsetOnline = offset) } + } + private fun loadFriends(offset: Int = currentOffset.value) { friendsUseCase.getFriends(count = LOAD_COUNT, offset = offset) .listenValue(viewModelScope) { state -> 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 55e23a84..5f6ff873 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 @@ -8,7 +8,11 @@ data class FriendsScreenState( val friends: List, val onlineFriends: List, val isPaginating: Boolean, - val isPaginationExhausted: Boolean + val isPaginationExhausted: Boolean, + val scrollIndex: Int, + val scrollOffset: Int, + val scrollIndexOnline: Int, + val scrollOffsetOnline: Int ) { companion object { @@ -17,7 +21,11 @@ data class FriendsScreenState( friends = emptyList(), onlineFriends = emptyList(), isPaginating = false, - isPaginationExhausted = false + isPaginationExhausted = false, + scrollIndex = 0, + scrollOffset = 0, + scrollIndexOnline = 0, + scrollOffsetOnline = 0, ) } } diff --git a/feature/friends/src/main/kotlin/dev/meloda/fast/friends/presentation/FriendsList.kt b/feature/friends/src/main/kotlin/dev/meloda/fast/friends/presentation/FriendsList.kt index 8b0d8a70..13f9c1b1 100644 --- a/feature/friends/src/main/kotlin/dev/meloda/fast/friends/presentation/FriendsList.kt +++ b/feature/friends/src/main/kotlin/dev/meloda/fast/friends/presentation/FriendsList.kt @@ -16,7 +16,9 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.IconButtonDefaults import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp @@ -35,8 +37,14 @@ fun FriendsList( listState: LazyListState, maxLines: Int, padding: PaddingValues, - onPhotoClicked: (url: String) -> Unit + onPhotoClicked: (url: String) -> Unit, + setCanScrollBackward: (Boolean) -> Unit ) { + LaunchedEffect(listState) { + snapshotFlow { listState.canScrollBackward } + .collect(setCanScrollBackward) + } + val coroutineScope = rememberCoroutineScope() val friends = uiFriends.toList() @@ -56,7 +64,6 @@ fun FriendsList( items = friends, key = UiFriend::userId, ) { friend -> - FriendItem( friend = friend, maxLines = maxLines, 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 be85945c..67cfa1f6 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 @@ -1,6 +1,5 @@ package dev.meloda.fast.friends.presentation -import android.content.SharedPreferences import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.FastOutLinearInEasing import androidx.compose.animation.core.animateFloatAsState @@ -35,6 +34,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue @@ -47,7 +47,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp -import androidx.core.content.edit import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil.imageLoader import coil.request.ImageRequest @@ -69,7 +68,6 @@ import dev.meloda.fast.ui.util.ImmutableList import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.debounce import org.koin.androidx.compose.koinViewModel -import org.koin.compose.koinInject import dev.meloda.fast.ui.R as UiR @Composable @@ -80,8 +78,6 @@ fun FriendsRoute( ) { val context = LocalContext.current - val prefs: SharedPreferences = koinInject() - val screenState by viewModel.screenState.collectAsStateWithLifecycle() val baseError by viewModel.baseError.collectAsStateWithLifecycle() val canPaginate by viewModel.canPaginate.collectAsStateWithLifecycle() @@ -101,23 +97,17 @@ fun FriendsRoute( screenState = screenState, baseError = baseError, canPaginate = canPaginate, - firstVisibleItemIndex = prefs.getInt("friends_all_scroll_position", 0), - firstVisibleItemScrollOffset = prefs.getInt("friends_all_scroll_offset", 0), onSessionExpiredLogOutButtonClicked = { onError(BaseError.SessionExpired) }, onPaginationConditionsMet = viewModel::onPaginationConditionsMet, onRefresh = viewModel::onRefresh, onPhotoClicked = onPhotoClicked, - onSaveScrollPosition = { index -> - prefs.edit { putInt("friends_all_scroll_position", index) } - }, - onSaveScrollOffsetPosition = { offset -> - prefs.edit { putInt("friends_all_scroll_offset", offset) } - } + setScrollIndex = viewModel::setScrollIndex, + setScrollOffset = viewModel::setScrollOffset, + setScrollIndexOnline = viewModel::setScrollIndexOnline, + setScrollOffsetOnline = viewModel::setScrollOffsetOnline, ) } - -// TODO: 13/07/2024, Danil Nikolaev: support for online @OptIn( ExperimentalMaterial3Api::class, ExperimentalHazeMaterialsApi::class @@ -127,14 +117,14 @@ fun FriendsScreen( screenState: FriendsScreenState = FriendsScreenState.EMPTY, baseError: BaseError? = null, canPaginate: Boolean = false, - firstVisibleItemIndex: Int = 0, - firstVisibleItemScrollOffset: Int = 0, onSessionExpiredLogOutButtonClicked: () -> Unit = {}, onPaginationConditionsMet: () -> Unit = {}, onRefresh: () -> Unit = {}, onPhotoClicked: (url: String) -> Unit = {}, - onSaveScrollPosition: (Int) -> Unit = {}, - onSaveScrollOffsetPosition: (Int) -> Unit = {} + setScrollIndex: (Int) -> Unit, + setScrollOffset: (Int) -> Unit, + setScrollIndexOnline: (Int) -> Unit, + setScrollOffsetOnline: (Int) -> Unit, ) { val currentTheme = LocalThemeConfig.current @@ -145,20 +135,36 @@ fun FriendsScreen( } val listState = rememberLazyListState( - initialFirstVisibleItemIndex = firstVisibleItemIndex, - initialFirstVisibleItemScrollOffset = firstVisibleItemScrollOffset + initialFirstVisibleItemIndex = screenState.scrollIndex, + initialFirstVisibleItemScrollOffset = screenState.scrollOffset + ) + val listStateOnline = rememberLazyListState( + initialFirstVisibleItemIndex = screenState.scrollIndexOnline, + initialFirstVisibleItemScrollOffset = screenState.scrollOffsetOnline ) LaunchedEffect(listState) { snapshotFlow { listState.firstVisibleItemIndex } .debounce(500L) - .collectLatest(onSaveScrollPosition) + .collectLatest(setScrollIndex) } LaunchedEffect(listState) { snapshotFlow { listState.firstVisibleItemScrollOffset } .debounce(500L) - .collectLatest(onSaveScrollOffsetPosition) + .collectLatest(setScrollOffset) + } + + LaunchedEffect(listStateOnline) { + snapshotFlow { listStateOnline.firstVisibleItemIndex } + .debounce(500L) + .collectLatest(setScrollIndexOnline) + } + + LaunchedEffect(listStateOnline) { + snapshotFlow { listStateOnline.firstVisibleItemScrollOffset } + .debounce(500L) + .collectLatest(setScrollOffsetOnline) } val paginationConditionMet by remember(canPaginate, listState) { @@ -177,8 +183,12 @@ fun FriendsScreen( val hazeState = LocalHazeState.current + var canScrollBackward by remember { + mutableStateOf(false) + } + val topBarContainerColorAlpha by animateFloatAsState( - targetValue = if (!currentTheme.enableBlur || !listState.canScrollBackward) 1f else 0f, + targetValue = if (!currentTheme.enableBlur || !canScrollBackward) 1f else 0f, label = "toolbarColorAlpha", animationSpec = tween( durationMillis = 200, @@ -187,8 +197,7 @@ fun FriendsScreen( ) val topBarContainerColor by animateColorAsState( - targetValue = - if (currentTheme.enableBlur || !listState.canScrollBackward) + targetValue = if (currentTheme.enableBlur || !canScrollBackward) MaterialTheme.colorScheme.surface else MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp), @@ -341,10 +350,13 @@ fun FriendsScreen( }.fillMaxSize(), screenState = screenState, uiFriends = ImmutableList.copyOf(friendsToDisplay), - listState = listState, + listState = if (index == 0) listState else listStateOnline, maxLines = maxLines, padding = padding, - onPhotoClicked = onPhotoClicked + onPhotoClicked = onPhotoClicked, + setCanScrollBackward = { can -> + canScrollBackward = can + } ) if (friendsToDisplay.isEmpty()) {