forked from melod1n/fast-messenger
separated screens for friends tab
This commit is contained in:
@@ -165,7 +165,6 @@ fun MainScreen(
|
|||||||
) {
|
) {
|
||||||
friendsScreen(
|
friendsScreen(
|
||||||
onError = onError,
|
onError = onError,
|
||||||
navController = navController,
|
|
||||||
onPhotoClicked = onPhotoClicked,
|
onPhotoClicked = onPhotoClicked,
|
||||||
onMessageClicked = onMessageClicked
|
onMessageClicked = onMessageClicked
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
|
|
||||||
// TODO: 13/07/2024, Danil Nikolaev: separate two lists and their pagination
|
|
||||||
interface FriendsViewModel {
|
interface FriendsViewModel {
|
||||||
|
|
||||||
val screenState: StateFlow<FriendsScreenState>
|
val screenState: StateFlow<FriendsScreenState>
|
||||||
@@ -33,19 +32,11 @@ interface FriendsViewModel {
|
|||||||
|
|
||||||
fun onErrorConsumed()
|
fun onErrorConsumed()
|
||||||
|
|
||||||
fun onTabSelected(tabIndex: Int)
|
|
||||||
|
|
||||||
fun setScrollIndex(index: Int)
|
fun setScrollIndex(index: Int)
|
||||||
fun setScrollOffset(offset: Int)
|
fun setScrollOffset(offset: Int)
|
||||||
fun setScrollIndexOnline(index: Int)
|
|
||||||
fun setScrollOffsetOnline(offset: Int)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class FriendsViewModelImpl(
|
abstract class BaseFriendsViewModelImpl : ViewModel(), FriendsViewModel {
|
||||||
private val friendsUseCase: FriendsUseCase,
|
|
||||||
private val userSettings: UserSettings,
|
|
||||||
private val loadUsersByIdsUseCase: LoadUsersByIdsUseCase
|
|
||||||
) : ViewModel(), FriendsViewModel {
|
|
||||||
|
|
||||||
override val screenState = MutableStateFlow(FriendsScreenState.EMPTY)
|
override val screenState = MutableStateFlow(FriendsScreenState.EMPTY)
|
||||||
|
|
||||||
@@ -54,13 +45,7 @@ class FriendsViewModelImpl(
|
|||||||
override val currentOffset = MutableStateFlow(0)
|
override val currentOffset = MutableStateFlow(0)
|
||||||
override val canPaginate = MutableStateFlow(false)
|
override val canPaginate = MutableStateFlow(false)
|
||||||
|
|
||||||
private val friends = MutableStateFlow<List<VkUser>>(emptyList())
|
protected val friends = MutableStateFlow<List<VkUser>>(emptyList())
|
||||||
|
|
||||||
init {
|
|
||||||
userSettings.useContactNames.listenValue(viewModelScope, ::updateFriendsNames)
|
|
||||||
|
|
||||||
loadFriends()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPaginationConditionsMet() {
|
override fun onPaginationConditionsMet() {
|
||||||
currentOffset.update { screenState.value.friends.size }
|
currentOffset.update { screenState.value.friends.size }
|
||||||
@@ -76,10 +61,6 @@ class FriendsViewModelImpl(
|
|||||||
baseError.setValue { null }
|
baseError.setValue { null }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onTabSelected(tabIndex: Int) {
|
|
||||||
screenState.setValue { old -> old.copy(selectedTabIndex = tabIndex) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setScrollIndex(index: Int) {
|
override fun setScrollIndex(index: Int) {
|
||||||
screenState.setValue { old -> old.copy(scrollIndex = index) }
|
screenState.setValue { old -> old.copy(scrollIndex = index) }
|
||||||
}
|
}
|
||||||
@@ -88,38 +69,75 @@ class FriendsViewModelImpl(
|
|||||||
screenState.setValue { old -> old.copy(scrollOffset = offset) }
|
screenState.setValue { old -> old.copy(scrollOffset = offset) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setScrollIndexOnline(index: Int) {
|
abstract fun loadFriends(offset: Int = currentOffset.value)
|
||||||
screenState.setValue { old -> old.copy(scrollIndexOnline = index) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setScrollOffsetOnline(offset: Int) {
|
protected fun handleError(error: State.Error) {
|
||||||
screenState.setValue { old -> old.copy(scrollOffsetOnline = offset) }
|
when (error) {
|
||||||
}
|
is State.Error.ApiError -> {
|
||||||
|
when (error.errorCode) {
|
||||||
private fun loadFriends(offset: Int = currentOffset.value) {
|
VkErrorCode.USER_AUTHORIZATION_FAILED -> {
|
||||||
friendsUseCase.getOnlineFriends(null, null)
|
baseError.setValue { BaseError.SessionExpired }
|
||||||
.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)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
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)
|
friendsUseCase.getFriends(count = LOAD_COUNT, offset = offset)
|
||||||
.listenValue(viewModelScope) { state ->
|
.listenValue(viewModelScope) { state ->
|
||||||
state.processState(
|
state.processState(
|
||||||
@@ -167,62 +185,48 @@ class FriendsViewModelImpl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun handleError(error: State.Error) {
|
class OnlineFriendsViewModelImpl(
|
||||||
when (error) {
|
private val friendsUseCase: FriendsUseCase,
|
||||||
is State.Error.ApiError -> {
|
private val userSettings: UserSettings,
|
||||||
when (error.errorCode) {
|
private val loadUsersByIdsUseCase: LoadUsersByIdsUseCase
|
||||||
VkErrorCode.USER_AUTHORIZATION_FAILED -> {
|
) : BaseFriendsViewModelImpl() {
|
||||||
baseError.setValue { BaseError.SessionExpired }
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
init {
|
||||||
baseError.setValue {
|
userSettings.useContactNames.listenValue(viewModelScope, ::updateFriendsNames)
|
||||||
BaseError.SimpleError(message = error.errorMessage)
|
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
|
package dev.meloda.fast.friends.di
|
||||||
|
|
||||||
import dev.meloda.fast.domain.FriendsUseCase
|
import dev.meloda.fast.domain.FriendsUseCase
|
||||||
import dev.meloda.fast.friends.FriendsViewModelImpl
|
|
||||||
import dev.meloda.fast.domain.FriendsUseCaseImpl
|
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.singleOf
|
||||||
import org.koin.core.module.dsl.viewModelOf
|
import org.koin.core.module.dsl.viewModelOf
|
||||||
import org.koin.dsl.bind
|
import org.koin.dsl.bind
|
||||||
@@ -11,4 +12,5 @@ import org.koin.dsl.module
|
|||||||
val friendsModule = module {
|
val friendsModule = module {
|
||||||
singleOf(::FriendsUseCaseImpl) bind FriendsUseCase::class
|
singleOf(::FriendsUseCaseImpl) bind FriendsUseCase::class
|
||||||
viewModelOf(::FriendsViewModelImpl)
|
viewModelOf(::FriendsViewModelImpl)
|
||||||
|
viewModelOf(::OnlineFriendsViewModelImpl)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,28 +7,20 @@ import dev.meloda.fast.ui.model.api.UiFriend
|
|||||||
data class FriendsScreenState(
|
data class FriendsScreenState(
|
||||||
val isLoading: Boolean,
|
val isLoading: Boolean,
|
||||||
val friends: List<UiFriend>,
|
val friends: List<UiFriend>,
|
||||||
val onlineFriends: List<UiFriend>,
|
|
||||||
val isPaginating: Boolean,
|
val isPaginating: Boolean,
|
||||||
val isPaginationExhausted: Boolean,
|
val isPaginationExhausted: Boolean,
|
||||||
val selectedTabIndex: Int,
|
|
||||||
val scrollIndex: Int,
|
val scrollIndex: Int,
|
||||||
val scrollOffset: Int,
|
val scrollOffset: Int,
|
||||||
val scrollIndexOnline: Int,
|
|
||||||
val scrollOffsetOnline: Int
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val EMPTY: FriendsScreenState = FriendsScreenState(
|
val EMPTY: FriendsScreenState = FriendsScreenState(
|
||||||
isLoading = true,
|
isLoading = true,
|
||||||
friends = emptyList(),
|
friends = emptyList(),
|
||||||
onlineFriends = emptyList(),
|
|
||||||
isPaginating = false,
|
isPaginating = false,
|
||||||
isPaginationExhausted = false,
|
isPaginationExhausted = false,
|
||||||
selectedTabIndex = 0,
|
|
||||||
scrollIndex = 0,
|
scrollIndex = 0,
|
||||||
scrollOffset = 0,
|
scrollOffset = 0
|
||||||
scrollIndexOnline = 0,
|
|
||||||
scrollOffsetOnline = 0,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
-9
@@ -1,13 +1,9 @@
|
|||||||
package dev.meloda.fast.friends.navigation
|
package dev.meloda.fast.friends.navigation
|
||||||
|
|
||||||
import androidx.navigation.NavController
|
|
||||||
import androidx.navigation.NavGraphBuilder
|
import androidx.navigation.NavGraphBuilder
|
||||||
import androidx.navigation.compose.composable
|
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.friends.presentation.FriendsRoute
|
||||||
import dev.meloda.fast.model.BaseError
|
import dev.meloda.fast.model.BaseError
|
||||||
import dev.meloda.fast.ui.extensions.sharedViewModel
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@@ -15,17 +11,12 @@ object Friends
|
|||||||
|
|
||||||
fun NavGraphBuilder.friendsScreen(
|
fun NavGraphBuilder.friendsScreen(
|
||||||
onError: (BaseError) -> Unit,
|
onError: (BaseError) -> Unit,
|
||||||
navController: NavController,
|
|
||||||
onPhotoClicked: (url: String) -> Unit,
|
onPhotoClicked: (url: String) -> Unit,
|
||||||
onMessageClicked: (userId: Int) -> Unit
|
onMessageClicked: (userId: Int) -> Unit
|
||||||
) {
|
) {
|
||||||
composable<Friends> {
|
composable<Friends> {
|
||||||
val viewModel: FriendsViewModel =
|
|
||||||
it.sharedViewModel<FriendsViewModelImpl>(navController = navController)
|
|
||||||
|
|
||||||
FriendsRoute(
|
FriendsRoute(
|
||||||
onError = onError,
|
onError = onError,
|
||||||
viewModel = viewModel,
|
|
||||||
onPhotoClicked = onPhotoClicked,
|
onPhotoClicked = onPhotoClicked,
|
||||||
onMessageClicked = onMessageClicked
|
onMessageClicked = onMessageClicked
|
||||||
)
|
)
|
||||||
|
|||||||
+88
-286
@@ -1,81 +1,64 @@
|
|||||||
package dev.meloda.fast.friends.presentation
|
package dev.meloda.fast.friends.presentation
|
||||||
|
|
||||||
import androidx.compose.animation.animateColorAsState
|
import android.content.Context
|
||||||
import androidx.compose.animation.core.FastOutLinearInEasing
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
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 androidx.compose.foundation.layout.calculateEndPadding
|
import androidx.compose.foundation.layout.calculateEndPadding
|
||||||
import androidx.compose.foundation.layout.calculateStartPadding
|
import androidx.compose.foundation.layout.calculateStartPadding
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.statusBars
|
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
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.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.PullToRefreshBox
|
||||||
import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults
|
import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults
|
||||||
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
|
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
|
||||||
import androidx.compose.material3.surfaceColorAtElevation
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.runtime.snapshotFlow
|
import androidx.compose.runtime.snapshotFlow
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.unit.LayoutDirection
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import coil.imageLoader
|
import coil.imageLoader
|
||||||
import coil.request.ImageRequest
|
import coil.request.ImageRequest
|
||||||
import dev.chrisbanes.haze.hazeEffect
|
|
||||||
import dev.chrisbanes.haze.hazeSource
|
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.FriendsViewModel
|
||||||
import dev.meloda.fast.friends.FriendsViewModelImpl
|
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.model.BaseError
|
||||||
|
import dev.meloda.fast.ui.R
|
||||||
import dev.meloda.fast.ui.components.ErrorView
|
import dev.meloda.fast.ui.components.ErrorView
|
||||||
import dev.meloda.fast.ui.components.FullScreenLoader
|
import dev.meloda.fast.ui.components.FullScreenLoader
|
||||||
import dev.meloda.fast.ui.components.NoItemsView
|
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.LocalHazeState
|
||||||
import dev.meloda.fast.ui.theme.LocalThemeConfig
|
import dev.meloda.fast.ui.theme.LocalThemeConfig
|
||||||
import dev.meloda.fast.ui.util.ImmutableList
|
import dev.meloda.fast.ui.util.ImmutableList
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.flow.debounce
|
import kotlinx.coroutines.flow.debounce
|
||||||
import org.koin.androidx.compose.koinViewModel
|
import org.koin.androidx.compose.koinViewModel
|
||||||
import dev.meloda.fast.ui.R as UiR
|
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun FriendsRoute(
|
fun FriendsScreen(
|
||||||
onError: (BaseError) -> Unit,
|
modifier: Modifier = Modifier,
|
||||||
onPhotoClicked: (url: String) -> Unit,
|
padding: PaddingValues,
|
||||||
onMessageClicked: (userId: Int) -> Unit,
|
tabIndex: Int,
|
||||||
viewModel: FriendsViewModel = koinViewModel<FriendsViewModelImpl>()
|
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 screenState by viewModel.screenState.collectAsStateWithLifecycle()
|
||||||
val baseError by viewModel.baseError.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 currentTheme = LocalThemeConfig.current
|
||||||
|
|
||||||
val maxLines by remember {
|
val maxLines by remember {
|
||||||
@@ -141,33 +87,17 @@ fun FriendsScreen(
|
|||||||
initialFirstVisibleItemIndex = screenState.scrollIndex,
|
initialFirstVisibleItemIndex = screenState.scrollIndex,
|
||||||
initialFirstVisibleItemScrollOffset = screenState.scrollOffset
|
initialFirstVisibleItemScrollOffset = screenState.scrollOffset
|
||||||
)
|
)
|
||||||
val listStateOnline = rememberLazyListState(
|
|
||||||
initialFirstVisibleItemIndex = screenState.scrollIndexOnline,
|
|
||||||
initialFirstVisibleItemScrollOffset = screenState.scrollOffsetOnline
|
|
||||||
)
|
|
||||||
|
|
||||||
LaunchedEffect(listState) {
|
LaunchedEffect(listState) {
|
||||||
snapshotFlow { listState.firstVisibleItemIndex }
|
snapshotFlow { listState.firstVisibleItemIndex }
|
||||||
.debounce(500L)
|
.debounce(250L)
|
||||||
.collectLatest(setScrollIndex)
|
.collectLatest(viewModel::setScrollIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(listState) {
|
LaunchedEffect(listState) {
|
||||||
snapshotFlow { listState.firstVisibleItemScrollOffset }
|
snapshotFlow { listState.firstVisibleItemScrollOffset }
|
||||||
.debounce(500L)
|
.debounce(250L)
|
||||||
.collectLatest(setScrollOffset)
|
.collectLatest(viewModel::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) {
|
val paginationConditionMet by remember(canPaginate, listState) {
|
||||||
@@ -180,209 +110,81 @@ fun FriendsScreen(
|
|||||||
|
|
||||||
LaunchedEffect(paginationConditionMet) {
|
LaunchedEffect(paginationConditionMet) {
|
||||||
if (paginationConditionMet && !screenState.isPaginating) {
|
if (paginationConditionMet && !screenState.isPaginating) {
|
||||||
onPaginationConditionsMet()
|
viewModel.onPaginationConditionsMet()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val hazeState = LocalHazeState.current
|
val hazeState = LocalHazeState.current
|
||||||
|
|
||||||
var canScrollBackward by remember {
|
baseError?.let { error ->
|
||||||
mutableStateOf(false)
|
when (error) {
|
||||||
}
|
is BaseError.SessionExpired -> {
|
||||||
|
ErrorView(
|
||||||
val topBarContainerColorAlpha by animateFloatAsState(
|
text = stringResource(R.string.session_expired),
|
||||||
targetValue = if (!currentTheme.enableBlur || !canScrollBackward) 1f else 0f,
|
buttonText = stringResource(R.string.action_log_out),
|
||||||
label = "toolbarColorAlpha",
|
onButtonClick = onSessionExpiredLogOutButtonClicked
|
||||||
animationSpec = tween(
|
)
|
||||||
durationMillis = 200,
|
}
|
||||||
easing = FastOutLinearInEasing
|
|
||||||
)
|
is BaseError.SimpleError -> {
|
||||||
)
|
ErrorView(
|
||||||
|
text = error.message,
|
||||||
val topBarContainerColor by animateColorAsState(
|
buttonText = stringResource(R.string.try_again),
|
||||||
targetValue = if (currentTheme.enableBlur || !canScrollBackward)
|
onButtonClick = viewModel::onRefresh
|
||||||
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()
|
|
||||||
)
|
)
|
||||||
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 -> {
|
return
|
||||||
ErrorView(
|
}
|
||||||
text = baseError.message,
|
|
||||||
buttonText = stringResource(UiR.string.try_again),
|
when {
|
||||||
onButtonClick = onRefresh
|
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()
|
if (screenState.friends.isEmpty()) {
|
||||||
|
NoItemsView(
|
||||||
else -> {
|
customText = if (tabIndex == 1) stringResource(R.string.no_online_friends) else null,
|
||||||
val pagerState = rememberPagerState(
|
buttonText = stringResource(R.string.action_refresh),
|
||||||
initialPage = screenState.selectedTabIndex
|
onButtonClick = viewModel::onRefresh
|
||||||
) {
|
)
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+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 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ minSdk = "23"
|
|||||||
targetSdk = "35"
|
targetSdk = "35"
|
||||||
compileSdk = "35"
|
compileSdk = "35"
|
||||||
versionCode = "9"
|
versionCode = "9"
|
||||||
versionName = "0.1.6"
|
versionName = "0.1.9"
|
||||||
|
|
||||||
agp = "8.9.0"
|
agp = "8.9.0"
|
||||||
converterMoshi = "2.11.0"
|
converterMoshi = "2.11.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user