separated screens for friends tab
This commit is contained in:
@@ -18,7 +18,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
// TODO: 13/07/2024, Danil Nikolaev: separate two lists and their pagination
|
||||
interface FriendsViewModel {
|
||||
|
||||
val screenState: StateFlow<FriendsScreenState>
|
||||
@@ -33,19 +32,11 @@ interface FriendsViewModel {
|
||||
|
||||
fun onErrorConsumed()
|
||||
|
||||
fun onTabSelected(tabIndex: Int)
|
||||
|
||||
fun setScrollIndex(index: Int)
|
||||
fun setScrollOffset(offset: Int)
|
||||
fun setScrollIndexOnline(index: Int)
|
||||
fun setScrollOffsetOnline(offset: Int)
|
||||
}
|
||||
|
||||
class FriendsViewModelImpl(
|
||||
private val friendsUseCase: FriendsUseCase,
|
||||
private val userSettings: UserSettings,
|
||||
private val loadUsersByIdsUseCase: LoadUsersByIdsUseCase
|
||||
) : ViewModel(), FriendsViewModel {
|
||||
abstract class BaseFriendsViewModelImpl : ViewModel(), FriendsViewModel {
|
||||
|
||||
override val screenState = MutableStateFlow(FriendsScreenState.EMPTY)
|
||||
|
||||
@@ -54,13 +45,7 @@ class FriendsViewModelImpl(
|
||||
override val currentOffset = MutableStateFlow(0)
|
||||
override val canPaginate = MutableStateFlow(false)
|
||||
|
||||
private val friends = MutableStateFlow<List<VkUser>>(emptyList())
|
||||
|
||||
init {
|
||||
userSettings.useContactNames.listenValue(viewModelScope, ::updateFriendsNames)
|
||||
|
||||
loadFriends()
|
||||
}
|
||||
protected val friends = MutableStateFlow<List<VkUser>>(emptyList())
|
||||
|
||||
override fun onPaginationConditionsMet() {
|
||||
currentOffset.update { screenState.value.friends.size }
|
||||
@@ -76,10 +61,6 @@ class FriendsViewModelImpl(
|
||||
baseError.setValue { null }
|
||||
}
|
||||
|
||||
override fun onTabSelected(tabIndex: Int) {
|
||||
screenState.setValue { old -> old.copy(selectedTabIndex = tabIndex) }
|
||||
}
|
||||
|
||||
override fun setScrollIndex(index: Int) {
|
||||
screenState.setValue { old -> old.copy(scrollIndex = index) }
|
||||
}
|
||||
@@ -88,38 +69,75 @@ class FriendsViewModelImpl(
|
||||
screenState.setValue { old -> old.copy(scrollOffset = offset) }
|
||||
}
|
||||
|
||||
override fun setScrollIndexOnline(index: Int) {
|
||||
screenState.setValue { old -> old.copy(scrollIndexOnline = index) }
|
||||
}
|
||||
abstract fun loadFriends(offset: Int = currentOffset.value)
|
||||
|
||||
override fun setScrollOffsetOnline(offset: Int) {
|
||||
screenState.setValue { old -> old.copy(scrollOffsetOnline = offset) }
|
||||
}
|
||||
|
||||
private fun loadFriends(offset: Int = currentOffset.value) {
|
||||
friendsUseCase.getOnlineFriends(null, null)
|
||||
.listenValue(viewModelScope) { state ->
|
||||
state.processState(
|
||||
error = ::handleError,
|
||||
success = { userIds ->
|
||||
loadUsersByIdsUseCase(userIds = userIds)
|
||||
.listenValue(viewModelScope) { state ->
|
||||
state.processState(
|
||||
error = ::handleError,
|
||||
success = { onlineFriends ->
|
||||
screenState.setValue { old ->
|
||||
old.copy(
|
||||
onlineFriends = onlineFriends.map {
|
||||
it.asPresentation(userSettings.useContactNames.value)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
protected fun handleError(error: State.Error) {
|
||||
when (error) {
|
||||
is State.Error.ApiError -> {
|
||||
when (error.errorCode) {
|
||||
VkErrorCode.USER_AUTHORIZATION_FAILED -> {
|
||||
baseError.setValue { BaseError.SessionExpired }
|
||||
}
|
||||
)
|
||||
|
||||
else -> {
|
||||
baseError.setValue {
|
||||
BaseError.SimpleError(message = error.errorMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
State.Error.ConnectionError -> {
|
||||
baseError.setValue {
|
||||
BaseError.SimpleError(message = "Connection error")
|
||||
}
|
||||
}
|
||||
|
||||
State.Error.InternalError -> {
|
||||
baseError.setValue {
|
||||
BaseError.SimpleError(message = "Internal error")
|
||||
}
|
||||
}
|
||||
|
||||
State.Error.UnknownError -> {
|
||||
baseError.setValue {
|
||||
BaseError.SimpleError(message = "Unknown error")
|
||||
}
|
||||
}
|
||||
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
protected fun updateFriendsNames(useContactNames: Boolean) {
|
||||
val friends = friends.value
|
||||
if (friends.isEmpty()) return
|
||||
|
||||
val uiFriends = friends.map { conversation ->
|
||||
conversation.asPresentation(useContactNames)
|
||||
}
|
||||
|
||||
screenState.setValue { old ->
|
||||
old.copy(friends = uiFriends)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val LOAD_COUNT = 30
|
||||
}
|
||||
}
|
||||
|
||||
class FriendsViewModelImpl(
|
||||
private val friendsUseCase: FriendsUseCase,
|
||||
private val userSettings: UserSettings
|
||||
) : BaseFriendsViewModelImpl() {
|
||||
|
||||
init {
|
||||
userSettings.useContactNames.listenValue(viewModelScope, ::updateFriendsNames)
|
||||
loadFriends()
|
||||
}
|
||||
|
||||
override fun loadFriends(offset: Int) {
|
||||
friendsUseCase.getFriends(count = LOAD_COUNT, offset = offset)
|
||||
.listenValue(viewModelScope) { state ->
|
||||
state.processState(
|
||||
@@ -167,62 +185,48 @@ class FriendsViewModelImpl(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleError(error: State.Error) {
|
||||
when (error) {
|
||||
is State.Error.ApiError -> {
|
||||
when (error.errorCode) {
|
||||
VkErrorCode.USER_AUTHORIZATION_FAILED -> {
|
||||
baseError.setValue { BaseError.SessionExpired }
|
||||
}
|
||||
class OnlineFriendsViewModelImpl(
|
||||
private val friendsUseCase: FriendsUseCase,
|
||||
private val userSettings: UserSettings,
|
||||
private val loadUsersByIdsUseCase: LoadUsersByIdsUseCase
|
||||
) : BaseFriendsViewModelImpl() {
|
||||
|
||||
else -> {
|
||||
baseError.setValue {
|
||||
BaseError.SimpleError(message = error.errorMessage)
|
||||
init {
|
||||
userSettings.useContactNames.listenValue(viewModelScope, ::updateFriendsNames)
|
||||
loadFriends()
|
||||
}
|
||||
|
||||
override fun loadFriends(offset: Int) {
|
||||
friendsUseCase.getOnlineFriends(null, null)
|
||||
.listenValue(viewModelScope) { onlineState ->
|
||||
onlineState.processState(
|
||||
error = ::handleError,
|
||||
success = { userIds ->
|
||||
loadUsersByIdsUseCase(userIds = userIds).listenValue(viewModelScope) { state ->
|
||||
state.processState(
|
||||
error = ::handleError,
|
||||
success = { onlineFriends ->
|
||||
screenState.setValue { old ->
|
||||
old.copy(
|
||||
friends = onlineFriends.map {
|
||||
it.asPresentation(userSettings.useContactNames.value)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
screenState.setValue { old ->
|
||||
old.copy(
|
||||
isLoading = offset == 0 && (onlineState.isLoading() || state.isLoading()),
|
||||
isPaginating = offset > 0 && (onlineState.isLoading() || state.isLoading())
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
State.Error.ConnectionError -> {
|
||||
baseError.setValue {
|
||||
BaseError.SimpleError(message = "Connection error")
|
||||
}
|
||||
}
|
||||
State.Error.InternalError -> {
|
||||
baseError.setValue {
|
||||
BaseError.SimpleError(message = "Internal error")
|
||||
}
|
||||
}
|
||||
State.Error.UnknownError -> {
|
||||
baseError.setValue {
|
||||
BaseError.SimpleError(message = "Unknown error")
|
||||
}
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateFriendsNames(useContactNames: Boolean) {
|
||||
val friends = friends.value
|
||||
if (friends.isEmpty()) return
|
||||
|
||||
val uiFriends = friends.map { conversation ->
|
||||
conversation.asPresentation(useContactNames)
|
||||
}
|
||||
|
||||
val onlineUiFriends = screenState.value.onlineFriends.mapNotNull { friend ->
|
||||
uiFriends.find { it.userId == friend.userId }
|
||||
}
|
||||
|
||||
screenState.setValue { old ->
|
||||
old.copy(
|
||||
friends = uiFriends,
|
||||
onlineFriends = onlineUiFriends
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val LOAD_COUNT = 15
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
package dev.meloda.fast.friends.di
|
||||
|
||||
import dev.meloda.fast.domain.FriendsUseCase
|
||||
import dev.meloda.fast.friends.FriendsViewModelImpl
|
||||
import dev.meloda.fast.domain.FriendsUseCaseImpl
|
||||
import dev.meloda.fast.friends.FriendsViewModelImpl
|
||||
import dev.meloda.fast.friends.OnlineFriendsViewModelImpl
|
||||
import org.koin.core.module.dsl.singleOf
|
||||
import org.koin.core.module.dsl.viewModelOf
|
||||
import org.koin.dsl.bind
|
||||
@@ -11,4 +12,5 @@ import org.koin.dsl.module
|
||||
val friendsModule = module {
|
||||
singleOf(::FriendsUseCaseImpl) bind FriendsUseCase::class
|
||||
viewModelOf(::FriendsViewModelImpl)
|
||||
viewModelOf(::OnlineFriendsViewModelImpl)
|
||||
}
|
||||
|
||||
@@ -7,28 +7,20 @@ import dev.meloda.fast.ui.model.api.UiFriend
|
||||
data class FriendsScreenState(
|
||||
val isLoading: Boolean,
|
||||
val friends: List<UiFriend>,
|
||||
val onlineFriends: List<UiFriend>,
|
||||
val isPaginating: Boolean,
|
||||
val isPaginationExhausted: Boolean,
|
||||
val selectedTabIndex: Int,
|
||||
val scrollIndex: Int,
|
||||
val scrollOffset: Int,
|
||||
val scrollIndexOnline: Int,
|
||||
val scrollOffsetOnline: Int
|
||||
) {
|
||||
|
||||
companion object {
|
||||
val EMPTY: FriendsScreenState = FriendsScreenState(
|
||||
isLoading = true,
|
||||
friends = emptyList(),
|
||||
onlineFriends = emptyList(),
|
||||
isPaginating = false,
|
||||
isPaginationExhausted = false,
|
||||
selectedTabIndex = 0,
|
||||
scrollIndex = 0,
|
||||
scrollOffset = 0,
|
||||
scrollIndexOnline = 0,
|
||||
scrollOffsetOnline = 0,
|
||||
scrollOffset = 0
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
-9
@@ -1,13 +1,9 @@
|
||||
package dev.meloda.fast.friends.navigation
|
||||
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.compose.composable
|
||||
import dev.meloda.fast.friends.FriendsViewModel
|
||||
import dev.meloda.fast.friends.FriendsViewModelImpl
|
||||
import dev.meloda.fast.friends.presentation.FriendsRoute
|
||||
import dev.meloda.fast.model.BaseError
|
||||
import dev.meloda.fast.ui.extensions.sharedViewModel
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
@@ -15,17 +11,12 @@ object Friends
|
||||
|
||||
fun NavGraphBuilder.friendsScreen(
|
||||
onError: (BaseError) -> Unit,
|
||||
navController: NavController,
|
||||
onPhotoClicked: (url: String) -> Unit,
|
||||
onMessageClicked: (userId: Int) -> Unit
|
||||
) {
|
||||
composable<Friends> {
|
||||
val viewModel: FriendsViewModel =
|
||||
it.sharedViewModel<FriendsViewModelImpl>(navController = navController)
|
||||
|
||||
FriendsRoute(
|
||||
onError = onError,
|
||||
viewModel = viewModel,
|
||||
onPhotoClicked = onPhotoClicked,
|
||||
onMessageClicked = onMessageClicked
|
||||
)
|
||||
|
||||
+88
-286
@@ -1,81 +1,64 @@
|
||||
package dev.meloda.fast.friends.presentation
|
||||
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.FastOutLinearInEasing
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import android.content.Context
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.calculateEndPadding
|
||||
import androidx.compose.foundation.layout.calculateStartPadding
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.statusBars
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
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.PrimaryTabRow
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Tab
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||
import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults
|
||||
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
|
||||
import androidx.compose.material3.surfaceColorAtElevation
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
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.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import coil.imageLoader
|
||||
import coil.request.ImageRequest
|
||||
import dev.chrisbanes.haze.hazeEffect
|
||||
import dev.chrisbanes.haze.hazeSource
|
||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||
import dev.chrisbanes.haze.materials.HazeMaterials
|
||||
import dev.meloda.fast.friends.FriendsViewModel
|
||||
import dev.meloda.fast.friends.FriendsViewModelImpl
|
||||
import dev.meloda.fast.friends.model.FriendsScreenState
|
||||
import dev.meloda.fast.friends.OnlineFriendsViewModelImpl
|
||||
import dev.meloda.fast.model.BaseError
|
||||
import dev.meloda.fast.ui.R
|
||||
import dev.meloda.fast.ui.components.ErrorView
|
||||
import dev.meloda.fast.ui.components.FullScreenLoader
|
||||
import dev.meloda.fast.ui.components.NoItemsView
|
||||
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 kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
import dev.meloda.fast.ui.R as UiR
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun FriendsRoute(
|
||||
onError: (BaseError) -> Unit,
|
||||
onPhotoClicked: (url: String) -> Unit,
|
||||
onMessageClicked: (userId: Int) -> Unit,
|
||||
viewModel: FriendsViewModel = koinViewModel<FriendsViewModelImpl>()
|
||||
fun FriendsScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
padding: PaddingValues,
|
||||
tabIndex: Int,
|
||||
onSessionExpiredLogOutButtonClicked: () -> Unit = {},
|
||||
onPhotoClicked: (url: String) -> Unit = {},
|
||||
onMessageClicked: (userId: Int) -> Unit = {},
|
||||
setCanScrollBackward: (Boolean) -> Unit = {}
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val context: Context = LocalContext.current
|
||||
val viewModel: FriendsViewModel =
|
||||
if (tabIndex == 0) {
|
||||
koinViewModel<FriendsViewModelImpl>()
|
||||
} else {
|
||||
koinViewModel<OnlineFriendsViewModelImpl>()
|
||||
}
|
||||
|
||||
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
|
||||
val baseError by viewModel.baseError.collectAsStateWithLifecycle()
|
||||
@@ -92,43 +75,6 @@ fun FriendsRoute(
|
||||
}
|
||||
}
|
||||
|
||||
FriendsScreen(
|
||||
screenState = screenState,
|
||||
baseError = baseError,
|
||||
canPaginate = canPaginate,
|
||||
onSessionExpiredLogOutButtonClicked = { onError(BaseError.SessionExpired) },
|
||||
onPaginationConditionsMet = viewModel::onPaginationConditionsMet,
|
||||
onRefresh = viewModel::onRefresh,
|
||||
onPhotoClicked = onPhotoClicked,
|
||||
onMessageClicked = onMessageClicked,
|
||||
setSelectedTabIndex = viewModel::onTabSelected,
|
||||
setScrollIndex = viewModel::setScrollIndex,
|
||||
setScrollOffset = viewModel::setScrollOffset,
|
||||
setScrollIndexOnline = viewModel::setScrollIndexOnline,
|
||||
setScrollOffsetOnline = viewModel::setScrollOffsetOnline
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(
|
||||
ExperimentalMaterial3Api::class,
|
||||
ExperimentalHazeMaterialsApi::class
|
||||
)
|
||||
@Composable
|
||||
fun FriendsScreen(
|
||||
screenState: FriendsScreenState = FriendsScreenState.EMPTY,
|
||||
baseError: BaseError? = null,
|
||||
canPaginate: Boolean = false,
|
||||
onSessionExpiredLogOutButtonClicked: () -> Unit = {},
|
||||
onPaginationConditionsMet: () -> Unit = {},
|
||||
onRefresh: () -> Unit = {},
|
||||
onPhotoClicked: (url: String) -> Unit = {},
|
||||
onMessageClicked: (userId: Int) -> Unit = {},
|
||||
setSelectedTabIndex: (Int) -> Unit = {},
|
||||
setScrollIndex: (Int) -> Unit = {},
|
||||
setScrollOffset: (Int) -> Unit = {},
|
||||
setScrollIndexOnline: (Int) -> Unit = {},
|
||||
setScrollOffsetOnline: (Int) -> Unit = {}
|
||||
) {
|
||||
val currentTheme = LocalThemeConfig.current
|
||||
|
||||
val maxLines by remember {
|
||||
@@ -141,33 +87,17 @@ fun FriendsScreen(
|
||||
initialFirstVisibleItemIndex = screenState.scrollIndex,
|
||||
initialFirstVisibleItemScrollOffset = screenState.scrollOffset
|
||||
)
|
||||
val listStateOnline = rememberLazyListState(
|
||||
initialFirstVisibleItemIndex = screenState.scrollIndexOnline,
|
||||
initialFirstVisibleItemScrollOffset = screenState.scrollOffsetOnline
|
||||
)
|
||||
|
||||
LaunchedEffect(listState) {
|
||||
snapshotFlow { listState.firstVisibleItemIndex }
|
||||
.debounce(500L)
|
||||
.collectLatest(setScrollIndex)
|
||||
.debounce(250L)
|
||||
.collectLatest(viewModel::setScrollIndex)
|
||||
}
|
||||
|
||||
LaunchedEffect(listState) {
|
||||
snapshotFlow { listState.firstVisibleItemScrollOffset }
|
||||
.debounce(500L)
|
||||
.collectLatest(setScrollOffset)
|
||||
}
|
||||
|
||||
LaunchedEffect(listStateOnline) {
|
||||
snapshotFlow { listStateOnline.firstVisibleItemIndex }
|
||||
.debounce(500L)
|
||||
.collectLatest(setScrollIndexOnline)
|
||||
}
|
||||
|
||||
LaunchedEffect(listStateOnline) {
|
||||
snapshotFlow { listStateOnline.firstVisibleItemScrollOffset }
|
||||
.debounce(500L)
|
||||
.collectLatest(setScrollOffsetOnline)
|
||||
.debounce(250L)
|
||||
.collectLatest(viewModel::setScrollOffset)
|
||||
}
|
||||
|
||||
val paginationConditionMet by remember(canPaginate, listState) {
|
||||
@@ -180,209 +110,81 @@ fun FriendsScreen(
|
||||
|
||||
LaunchedEffect(paginationConditionMet) {
|
||||
if (paginationConditionMet && !screenState.isPaginating) {
|
||||
onPaginationConditionsMet()
|
||||
viewModel.onPaginationConditionsMet()
|
||||
}
|
||||
}
|
||||
|
||||
val hazeState = LocalHazeState.current
|
||||
|
||||
var canScrollBackward by remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
val topBarContainerColorAlpha by animateFloatAsState(
|
||||
targetValue = if (!currentTheme.enableBlur || !canScrollBackward) 1f else 0f,
|
||||
label = "toolbarColorAlpha",
|
||||
animationSpec = tween(
|
||||
durationMillis = 200,
|
||||
easing = FastOutLinearInEasing
|
||||
)
|
||||
)
|
||||
|
||||
val topBarContainerColor by animateColorAsState(
|
||||
targetValue = if (currentTheme.enableBlur || !canScrollBackward)
|
||||
MaterialTheme.colorScheme.surface
|
||||
else
|
||||
MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp),
|
||||
label = "toolbarColorAlpha",
|
||||
animationSpec = tween(
|
||||
durationMillis = 200,
|
||||
easing = FastOutLinearInEasing
|
||||
)
|
||||
)
|
||||
|
||||
val tabItems = remember {
|
||||
listOf(
|
||||
TabItem(
|
||||
titleResId = UiR.string.title_friends_all,
|
||||
unselectedIconResId = null,
|
||||
selectedIconResId = null
|
||||
),
|
||||
TabItem(
|
||||
titleResId = UiR.string.title_friends_online,
|
||||
unselectedIconResId = null,
|
||||
selectedIconResId = null
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentWindowInsets = WindowInsets.statusBars,
|
||||
topBar = {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.then(
|
||||
if (currentTheme.enableBlur) {
|
||||
Modifier.hazeEffect(
|
||||
state = hazeState,
|
||||
style = HazeMaterials.thick()
|
||||
)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
)
|
||||
.background(topBarContainerColor.copy(alpha = topBarContainerColorAlpha))
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(id = UiR.string.title_friends),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = MaterialTheme.typography.headlineSmall
|
||||
)
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = Color.Transparent
|
||||
),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
baseError?.let { error ->
|
||||
when (error) {
|
||||
is BaseError.SessionExpired -> {
|
||||
ErrorView(
|
||||
text = stringResource(R.string.session_expired),
|
||||
buttonText = stringResource(R.string.action_log_out),
|
||||
onButtonClick = onSessionExpiredLogOutButtonClicked
|
||||
)
|
||||
}
|
||||
|
||||
is BaseError.SimpleError -> {
|
||||
ErrorView(
|
||||
text = error.message,
|
||||
buttonText = stringResource(R.string.try_again),
|
||||
onButtonClick = viewModel::onRefresh
|
||||
)
|
||||
PrimaryTabRow(
|
||||
selectedTabIndex = screenState.selectedTabIndex,
|
||||
modifier = Modifier,
|
||||
containerColor = Color.Transparent
|
||||
) {
|
||||
tabItems.forEachIndexed { index, item ->
|
||||
Tab(
|
||||
selected = index == screenState.selectedTabIndex,
|
||||
onClick = {
|
||||
if (screenState.selectedTabIndex != index) {
|
||||
setSelectedTabIndex(index)
|
||||
}
|
||||
},
|
||||
text = {
|
||||
item.titleResId?.let { resId ->
|
||||
Text(text = stringResource(id = resId))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
) { padding ->
|
||||
when {
|
||||
baseError != null -> {
|
||||
when (baseError) {
|
||||
is BaseError.SessionExpired -> {
|
||||
ErrorView(
|
||||
text = stringResource(UiR.string.session_expired),
|
||||
buttonText = stringResource(UiR.string.action_log_out),
|
||||
onButtonClick = onSessionExpiredLogOutButtonClicked
|
||||
)
|
||||
}
|
||||
|
||||
is BaseError.SimpleError -> {
|
||||
ErrorView(
|
||||
text = baseError.message,
|
||||
buttonText = stringResource(UiR.string.try_again),
|
||||
onButtonClick = onRefresh
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
when {
|
||||
screenState.isLoading && screenState.friends.isEmpty() -> FullScreenLoader()
|
||||
|
||||
else -> {
|
||||
val pullToRefreshState = rememberPullToRefreshState()
|
||||
|
||||
PullToRefreshBox(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.padding(start = padding.calculateStartPadding(LayoutDirection.Ltr))
|
||||
.padding(end = padding.calculateEndPadding(LayoutDirection.Ltr))
|
||||
.padding(bottom = padding.calculateBottomPadding()),
|
||||
state = pullToRefreshState,
|
||||
isRefreshing = screenState.isLoading,
|
||||
onRefresh = viewModel::onRefresh,
|
||||
indicator = {
|
||||
PullToRefreshDefaults.Indicator(
|
||||
state = pullToRefreshState,
|
||||
isRefreshing = screenState.isLoading,
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopCenter)
|
||||
.padding(top = padding.calculateTopPadding()),
|
||||
)
|
||||
}
|
||||
}
|
||||
) {
|
||||
FriendsList(
|
||||
modifier = if (currentTheme.enableBlur) {
|
||||
Modifier.hazeSource(state = hazeState)
|
||||
} else {
|
||||
Modifier
|
||||
}.fillMaxSize(),
|
||||
screenState = screenState,
|
||||
uiFriends = ImmutableList.copyOf(screenState.friends),
|
||||
listState = listState,
|
||||
maxLines = maxLines,
|
||||
padding = padding,
|
||||
onPhotoClicked = onPhotoClicked,
|
||||
onMessageClicked = onMessageClicked,
|
||||
setCanScrollBackward = setCanScrollBackward
|
||||
)
|
||||
|
||||
screenState.isLoading && screenState.friends.isEmpty() -> FullScreenLoader()
|
||||
|
||||
else -> {
|
||||
val pagerState = rememberPagerState(
|
||||
initialPage = screenState.selectedTabIndex
|
||||
) {
|
||||
tabItems.size
|
||||
}
|
||||
|
||||
LaunchedEffect(screenState.selectedTabIndex) {
|
||||
pagerState.animateScrollToPage(screenState.selectedTabIndex)
|
||||
}
|
||||
|
||||
LaunchedEffect(pagerState) {
|
||||
snapshotFlow { pagerState.currentPage }
|
||||
.collect(setSelectedTabIndex)
|
||||
}
|
||||
|
||||
val pullToRefreshState = rememberPullToRefreshState()
|
||||
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
HorizontalPager(
|
||||
state = pagerState,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) { index ->
|
||||
PullToRefreshBox(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(start = padding.calculateStartPadding(LayoutDirection.Ltr))
|
||||
.padding(end = padding.calculateEndPadding(LayoutDirection.Ltr))
|
||||
.padding(bottom = padding.calculateBottomPadding()),
|
||||
state = pullToRefreshState,
|
||||
isRefreshing = screenState.isLoading,
|
||||
onRefresh = onRefresh,
|
||||
indicator = {
|
||||
PullToRefreshDefaults.Indicator(
|
||||
state = pullToRefreshState,
|
||||
isRefreshing = screenState.isLoading,
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopCenter)
|
||||
.padding(top = padding.calculateTopPadding()),
|
||||
)
|
||||
}
|
||||
) {
|
||||
val friendsToDisplay = remember(index) {
|
||||
if (index == 0) {
|
||||
screenState.friends
|
||||
} else {
|
||||
screenState.onlineFriends
|
||||
}
|
||||
}
|
||||
|
||||
FriendsList(
|
||||
modifier = if (currentTheme.enableBlur) {
|
||||
Modifier.hazeSource(state = hazeState)
|
||||
} else {
|
||||
Modifier
|
||||
}.fillMaxSize(),
|
||||
screenState = screenState,
|
||||
uiFriends = ImmutableList.copyOf(friendsToDisplay),
|
||||
listState = if (index == 0) listState else listStateOnline,
|
||||
maxLines = maxLines,
|
||||
padding = padding,
|
||||
onPhotoClicked = onPhotoClicked,
|
||||
onMessageClicked = onMessageClicked,
|
||||
setCanScrollBackward = { can ->
|
||||
canScrollBackward = can
|
||||
}
|
||||
)
|
||||
|
||||
if (friendsToDisplay.isEmpty()) {
|
||||
NoItemsView(
|
||||
customText = if (index == 1) stringResource(UiR.string.no_online_friends) else null,
|
||||
buttonText = stringResource(UiR.string.action_refresh),
|
||||
onButtonClick = onRefresh
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (screenState.friends.isEmpty()) {
|
||||
NoItemsView(
|
||||
customText = if (tabIndex == 1) stringResource(R.string.no_online_friends) else null,
|
||||
buttonText = stringResource(R.string.action_refresh),
|
||||
onButtonClick = viewModel::onRefresh
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+187
@@ -0,0 +1,187 @@
|
||||
package dev.meloda.fast.friends.presentation
|
||||
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.FastOutLinearInEasing
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
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.MaterialTheme
|
||||
import androidx.compose.material3.PrimaryTabRow
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Tab
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.surfaceColorAtElevation
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
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
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.chrisbanes.haze.hazeEffect
|
||||
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.model.TabItem
|
||||
import dev.meloda.fast.ui.theme.LocalHazeState
|
||||
import dev.meloda.fast.ui.theme.LocalThemeConfig
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalHazeMaterialsApi::class)
|
||||
@Composable
|
||||
fun FriendsRoute(
|
||||
onError: (BaseError) -> Unit,
|
||||
onPhotoClicked: (url: String) -> Unit,
|
||||
onMessageClicked: (userId: Int) -> Unit,
|
||||
) {
|
||||
var selectedTabIndex by rememberSaveable {
|
||||
mutableIntStateOf(0)
|
||||
}
|
||||
|
||||
val currentTheme = LocalThemeConfig.current
|
||||
val hazeState = LocalHazeState.current
|
||||
|
||||
var canScrollBackward by remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
val topBarContainerColorAlpha by animateFloatAsState(
|
||||
targetValue = if (!currentTheme.enableBlur || !canScrollBackward) 1f else 0f,
|
||||
label = "toolbarColorAlpha",
|
||||
animationSpec = tween(
|
||||
durationMillis = 200,
|
||||
easing = FastOutLinearInEasing
|
||||
)
|
||||
)
|
||||
|
||||
val topBarContainerColor by animateColorAsState(
|
||||
targetValue = if (currentTheme.enableBlur || !canScrollBackward)
|
||||
MaterialTheme.colorScheme.surface
|
||||
else
|
||||
MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp),
|
||||
label = "toolbarColorAlpha",
|
||||
animationSpec = tween(
|
||||
durationMillis = 200,
|
||||
easing = FastOutLinearInEasing
|
||||
)
|
||||
)
|
||||
|
||||
val tabItems = remember {
|
||||
listOf(
|
||||
TabItem(
|
||||
titleResId = R.string.title_friends_all,
|
||||
unselectedIconResId = null,
|
||||
selectedIconResId = null
|
||||
),
|
||||
TabItem(
|
||||
titleResId = R.string.title_friends_online,
|
||||
unselectedIconResId = null,
|
||||
selectedIconResId = null
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentWindowInsets = WindowInsets.statusBars,
|
||||
topBar = {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.then(
|
||||
if (currentTheme.enableBlur) {
|
||||
Modifier.hazeEffect(
|
||||
state = hazeState,
|
||||
style = HazeMaterials.thick()
|
||||
)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
)
|
||||
.background(topBarContainerColor.copy(alpha = topBarContainerColorAlpha))
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(id = R.string.title_friends),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = MaterialTheme.typography.headlineSmall
|
||||
)
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = Color.Transparent
|
||||
),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
PrimaryTabRow(
|
||||
selectedTabIndex = selectedTabIndex,
|
||||
modifier = Modifier,
|
||||
containerColor = Color.Transparent
|
||||
) {
|
||||
tabItems.forEachIndexed { index, item ->
|
||||
Tab(
|
||||
selected = index == selectedTabIndex,
|
||||
onClick = {
|
||||
if (selectedTabIndex != index) {
|
||||
selectedTabIndex = index
|
||||
}
|
||||
},
|
||||
text = {
|
||||
item.titleResId?.let { resId ->
|
||||
Text(text = stringResource(id = resId))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
) { padding ->
|
||||
val pagerState = rememberPagerState(
|
||||
initialPage = selectedTabIndex
|
||||
) {
|
||||
tabItems.size
|
||||
}
|
||||
|
||||
LaunchedEffect(selectedTabIndex) {
|
||||
pagerState.animateScrollToPage(selectedTabIndex)
|
||||
}
|
||||
|
||||
LaunchedEffect(pagerState) {
|
||||
snapshotFlow { pagerState.currentPage }
|
||||
.collect { selectedTabIndex = it }
|
||||
}
|
||||
|
||||
HorizontalPager(
|
||||
state = pagerState,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) { index ->
|
||||
FriendsScreen(
|
||||
padding = padding,
|
||||
tabIndex = index,
|
||||
onSessionExpiredLogOutButtonClicked = { onError(BaseError.SessionExpired) },
|
||||
onPhotoClicked = onPhotoClicked,
|
||||
onMessageClicked = onMessageClicked,
|
||||
setCanScrollBackward = { canScrollBackward = it }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user