forked from melod1n/fast-messenger
Upstream changes (#23)
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,175 @@
|
||||
package com.meloda.app.fast.friends
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.meloda.app.fast.common.extensions.listenValue
|
||||
import com.meloda.app.fast.common.extensions.setValue
|
||||
import com.meloda.app.fast.data.State
|
||||
import com.meloda.app.fast.data.api.friends.FriendsUseCase
|
||||
import com.meloda.app.fast.data.processState
|
||||
import com.meloda.app.fast.datastore.UserSettings
|
||||
import com.meloda.app.fast.friends.model.FriendsScreenState
|
||||
import com.meloda.app.fast.friends.model.UiFriend
|
||||
import com.meloda.app.fast.friends.util.asPresentation
|
||||
import com.meloda.app.fast.model.BaseError
|
||||
import com.meloda.app.fast.model.api.domain.VkUser
|
||||
import com.meloda.app.fast.network.VkErrorCodes
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
interface FriendsViewModel {
|
||||
|
||||
val screenState: StateFlow<FriendsScreenState>
|
||||
val uiFriends: StateFlow<List<UiFriend>>
|
||||
val uiOnlineFriends: StateFlow<List<UiFriend>>
|
||||
val baseError: StateFlow<BaseError?>
|
||||
val imagesToPreload: StateFlow<List<String>>
|
||||
val currentOffset: StateFlow<Int>
|
||||
val canPaginate: StateFlow<Boolean>
|
||||
|
||||
fun onMetPaginationCondition()
|
||||
|
||||
fun onRefresh()
|
||||
|
||||
fun onErrorConsumed()
|
||||
}
|
||||
|
||||
class FriendsViewModelImpl(
|
||||
private val friendsUseCase: FriendsUseCase,
|
||||
private val userSettings: UserSettings
|
||||
) : ViewModel(), FriendsViewModel {
|
||||
|
||||
override val screenState = MutableStateFlow(FriendsScreenState.EMPTY)
|
||||
|
||||
override val uiFriends = screenState.map { it.friends }
|
||||
.stateIn(viewModelScope, SharingStarted.Lazily, emptyList())
|
||||
|
||||
override val uiOnlineFriends = MutableStateFlow<List<UiFriend>>(emptyList())
|
||||
|
||||
override val baseError = MutableStateFlow<BaseError?>(null)
|
||||
override val imagesToPreload = MutableStateFlow<List<String>>(emptyList())
|
||||
override val currentOffset = MutableStateFlow(0)
|
||||
override val canPaginate = MutableStateFlow(false)
|
||||
|
||||
private val friends = MutableStateFlow<List<VkUser>>(emptyList())
|
||||
|
||||
init {
|
||||
userSettings.useContactNames.listenValue(::updateFriendsNames)
|
||||
|
||||
loadFriends()
|
||||
}
|
||||
|
||||
override fun onMetPaginationCondition() {
|
||||
currentOffset.update { screenState.value.friends.size }
|
||||
loadFriends()
|
||||
}
|
||||
|
||||
override fun onRefresh() {
|
||||
loadFriends(offset = 0)
|
||||
}
|
||||
|
||||
override fun onErrorConsumed() {
|
||||
baseError.setValue { null }
|
||||
}
|
||||
|
||||
private fun loadFriends(offset: Int = currentOffset.value) {
|
||||
friendsUseCase.getAllFriends(count = 30, offset = offset).listenValue { state ->
|
||||
state.processState(
|
||||
error = { error ->
|
||||
when (error) {
|
||||
is State.Error.ApiError -> {
|
||||
val (code, message) = error
|
||||
|
||||
when (code) {
|
||||
VkErrorCodes.UserAuthorizationFailed -> {
|
||||
baseError.setValue { BaseError.SessionExpired }
|
||||
}
|
||||
|
||||
else -> {
|
||||
Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
State.Error.ConnectionError -> TODO()
|
||||
State.Error.InternalError -> TODO()
|
||||
is State.Error.OAuthError -> TODO()
|
||||
State.Error.Unknown -> TODO()
|
||||
}
|
||||
},
|
||||
success = { info ->
|
||||
val response = info.friends
|
||||
val itemsCountSufficient = response.size == 30
|
||||
canPaginate.setValue { itemsCountSufficient }
|
||||
|
||||
val paginationExhausted = !itemsCountSufficient &&
|
||||
screenState.value.friends.size >= 30
|
||||
|
||||
imagesToPreload.setValue {
|
||||
response.mapNotNull(VkUser::photo100)
|
||||
}
|
||||
|
||||
friendsUseCase.storeUsers(response)
|
||||
|
||||
val loadedFriends = response.map {
|
||||
it.asPresentation(userSettings.useContactNames.value)
|
||||
}
|
||||
val loadedOnlineFriends = info.onlineFriends.map {
|
||||
it.asPresentation(userSettings.useContactNames.value)
|
||||
}
|
||||
|
||||
val newState = screenState.value.copy(
|
||||
isPaginationExhausted = paginationExhausted
|
||||
)
|
||||
|
||||
if (offset == 0) {
|
||||
friends.emit(response)
|
||||
screenState.setValue {
|
||||
newState.copy(friends = loadedFriends)
|
||||
}
|
||||
uiOnlineFriends.setValue { loadedOnlineFriends }
|
||||
} else {
|
||||
friends.emit(friends.value.plus(response))
|
||||
screenState.setValue {
|
||||
newState.copy(
|
||||
friends = newState.friends.plus(loadedFriends)
|
||||
)
|
||||
}
|
||||
uiOnlineFriends.setValue { old ->
|
||||
old.plus(loadedFriends)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
screenState.setValue { old ->
|
||||
old.copy(
|
||||
isLoading = offset == 0 && state.isLoading(),
|
||||
isPaginating = offset > 0 && state.isLoading()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateFriendsNames(useContactNames: Boolean) {
|
||||
val friends = friends.value
|
||||
if (friends.isEmpty()) return
|
||||
|
||||
val uiFriends = friends.map { conversation ->
|
||||
conversation.asPresentation(useContactNames)
|
||||
}
|
||||
|
||||
val onlineUiFriends = uiOnlineFriends.value.mapNotNull { friend ->
|
||||
uiFriends.find { it.userId == friend.userId }
|
||||
}
|
||||
|
||||
screenState.setValue { old ->
|
||||
old.copy(friends = uiFriends)
|
||||
}
|
||||
uiOnlineFriends.setValue { onlineUiFriends }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.meloda.app.fast.friends.di
|
||||
|
||||
import com.meloda.app.fast.data.api.friends.FriendsUseCase
|
||||
import com.meloda.app.fast.friends.FriendsViewModelImpl
|
||||
import com.meloda.app.fast.friends.domain.FriendsUseCaseImpl
|
||||
import org.koin.androidx.viewmodel.dsl.viewModelOf
|
||||
import org.koin.core.module.dsl.singleOf
|
||||
import org.koin.dsl.bind
|
||||
import org.koin.dsl.module
|
||||
|
||||
val friendsModule = module {
|
||||
singleOf(::FriendsUseCaseImpl) bind FriendsUseCase::class
|
||||
|
||||
viewModelOf(::FriendsViewModelImpl)
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
package com.meloda.app.fast.friends.domain
|
||||
|
||||
import com.meloda.app.fast.data.State
|
||||
import com.meloda.app.fast.data.api.friends.FriendsRepository
|
||||
import com.meloda.app.fast.data.api.friends.FriendsUseCase
|
||||
import com.meloda.app.fast.data.mapToState
|
||||
import com.meloda.app.fast.model.FriendsInfo
|
||||
import com.meloda.app.fast.model.api.domain.VkUser
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
|
||||
class FriendsUseCaseImpl(private val repository: FriendsRepository) : FriendsUseCase {
|
||||
|
||||
override fun getAllFriends(count: Int?, offset: Int?): Flow<State<FriendsInfo>> = flow {
|
||||
emit(State.Loading)
|
||||
|
||||
val newState = repository.getAllFriends(count, offset).mapToState()
|
||||
emit(newState)
|
||||
}
|
||||
|
||||
override fun getFriends(
|
||||
count: Int?, offset: Int?
|
||||
): Flow<State<List<VkUser>>> = flow {
|
||||
emit(State.Loading)
|
||||
|
||||
val newState = repository.getFriends(count, offset).mapToState()
|
||||
emit(newState)
|
||||
}
|
||||
|
||||
override fun getOnlineFriends(
|
||||
count: Int?, offset: Int?
|
||||
): Flow<State<List<Int>>> = flow {
|
||||
emit(State.Loading)
|
||||
|
||||
val newState = repository.getOnlineFriends(count, offset).mapToState()
|
||||
emit(newState)
|
||||
}
|
||||
|
||||
override suspend fun storeUsers(users: List<VkUser>) {
|
||||
repository.storeUsers(users)
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package com.meloda.app.fast.friends.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
|
||||
@Immutable
|
||||
data class FriendsScreenState(
|
||||
val isLoading: Boolean,
|
||||
val friends: List<UiFriend>,
|
||||
val isPaginating: Boolean,
|
||||
val isPaginationExhausted: Boolean
|
||||
) {
|
||||
|
||||
companion object {
|
||||
val EMPTY: FriendsScreenState = FriendsScreenState(
|
||||
isLoading = true,
|
||||
friends = emptyList(),
|
||||
isPaginating = false,
|
||||
isPaginationExhausted = false
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.meloda.app.fast.friends.model
|
||||
|
||||
enum class OnlineState {
|
||||
OFFLINE, ONLINE, ONLINE_MOBILE
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.meloda.app.fast.friends.model
|
||||
|
||||
import com.meloda.app.fast.common.UiImage
|
||||
import com.meloda.app.fast.model.api.domain.OnlineStatus
|
||||
|
||||
data class UiFriend(
|
||||
val userId: Int,
|
||||
val avatar: UiImage?,
|
||||
val title: String,
|
||||
val onlineStatus: OnlineStatus
|
||||
)
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
package com.meloda.app.fast.friends.navigation
|
||||
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.compose.composable
|
||||
import com.meloda.app.fast.common.extensions.navigation.sharedViewModel
|
||||
import com.meloda.app.fast.friends.FriendsViewModel
|
||||
import com.meloda.app.fast.friends.FriendsViewModelImpl
|
||||
import com.meloda.app.fast.friends.presentation.FriendsScreen
|
||||
import com.meloda.app.fast.model.BaseError
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
object Friends
|
||||
|
||||
fun NavGraphBuilder.friendsRoute(
|
||||
onError: (BaseError) -> Unit,
|
||||
navController: NavController
|
||||
) {
|
||||
composable<Friends> {
|
||||
val viewModel: FriendsViewModel =
|
||||
it.sharedViewModel<FriendsViewModelImpl>(navController = navController)
|
||||
|
||||
FriendsScreen(
|
||||
onError = onError,
|
||||
viewModel = viewModel
|
||||
)
|
||||
}
|
||||
}
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
package com.meloda.app.fast.friends.presentation
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import coil.compose.AsyncImage
|
||||
import coil.request.ImageRequest
|
||||
import com.meloda.app.fast.designsystem.R
|
||||
import com.meloda.app.fast.friends.model.UiFriend
|
||||
|
||||
@Composable
|
||||
fun FriendItem(
|
||||
modifier: Modifier = Modifier,
|
||||
friend: UiFriend,
|
||||
maxLines: Int
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
Row(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
|
||||
val friendAvatar = friend.avatar?.extractUrl()
|
||||
|
||||
Box(modifier = Modifier.size(56.dp)) {
|
||||
if (friendAvatar == null) {
|
||||
Image(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.clip(CircleShape),
|
||||
painter = painterResource(id = R.drawable.ic_account_circle_cut),
|
||||
contentDescription = "Avatar",
|
||||
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onBackground)
|
||||
)
|
||||
} else {
|
||||
AsyncImage(
|
||||
model = ImageRequest.Builder(context)
|
||||
.data(friendAvatar)
|
||||
.crossfade(true)
|
||||
.build(),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.clip(CircleShape),
|
||||
placeholder = painterResource(id = R.drawable.ic_account_circle_cut)
|
||||
)
|
||||
}
|
||||
|
||||
if (friend.onlineStatus.isOnline()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.clip(CircleShape)
|
||||
.size(18.dp)
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
.padding(2.dp)
|
||||
.align(Alignment.BottomEnd)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.clip(CircleShape)
|
||||
.matchParentSize()
|
||||
.background(MaterialTheme.colorScheme.primary)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
|
||||
Text(
|
||||
text = friend.title,
|
||||
minLines = 1,
|
||||
maxLines = maxLines,
|
||||
style = MaterialTheme.typography.headlineSmall.copy(fontSize = 20.sp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
}
|
||||
}
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
package com.meloda.app.fast.friends.presentation
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.KeyboardArrowUp
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.IconButtonDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.meloda.app.fast.designsystem.ImmutableList
|
||||
import com.meloda.app.fast.friends.model.FriendsScreenState
|
||||
import com.meloda.app.fast.friends.model.UiFriend
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun FriendsList(
|
||||
modifier: Modifier = Modifier,
|
||||
screenState: FriendsScreenState,
|
||||
uiFriends: ImmutableList<UiFriend>,
|
||||
listState: LazyListState,
|
||||
maxLines: Int,
|
||||
padding: PaddingValues
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
val friends = uiFriends.toList()
|
||||
|
||||
LazyColumn(
|
||||
modifier = modifier,
|
||||
state = listState
|
||||
) {
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(padding.calculateTopPadding()))
|
||||
Spacer(modifier = Modifier.height(64.dp))
|
||||
}
|
||||
|
||||
items(
|
||||
items = friends,
|
||||
key = UiFriend::userId,
|
||||
) { friend ->
|
||||
|
||||
FriendItem(
|
||||
friend = friend,
|
||||
maxLines = maxLines
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
|
||||
item {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.animateItem(fadeInSpec = null, fadeOutSpec = null)
|
||||
.navigationBarsPadding(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
if (screenState.isPaginating) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
|
||||
if (screenState.isPaginationExhausted) {
|
||||
IconButton(
|
||||
onClick = {
|
||||
coroutineScope.launch(Dispatchers.Main) {
|
||||
listState.scrollToItem(14)
|
||||
listState.animateScrollToItem(0)
|
||||
}
|
||||
},
|
||||
colors = IconButtonDefaults.filledIconButtonColors()
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.KeyboardArrowUp,
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+308
@@ -0,0 +1,308 @@
|
||||
package com.meloda.app.fast.friends.presentation
|
||||
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
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.calculateStartPadding
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
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.Scaffold
|
||||
import androidx.compose.material3.Tab
|
||||
import androidx.compose.material3.TabRow
|
||||
import androidx.compose.material3.TabRowDefaults
|
||||
import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.pulltorefresh.PullToRefreshContainer
|
||||
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.mutableIntStateOf
|
||||
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.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
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 com.meloda.app.fast.designsystem.ImmutableList
|
||||
import com.meloda.app.fast.designsystem.LocalTheme
|
||||
import com.meloda.app.fast.designsystem.TabItem
|
||||
import com.meloda.app.fast.designsystem.components.BlurrableTopAppBar
|
||||
import com.meloda.app.fast.designsystem.components.FullScreenLoader
|
||||
import com.meloda.app.fast.designsystem.components.NoItemsView
|
||||
import com.meloda.app.fast.friends.FriendsViewModel
|
||||
import com.meloda.app.fast.friends.FriendsViewModelImpl
|
||||
import com.meloda.app.fast.model.BaseError
|
||||
import com.meloda.app.fast.ui.ErrorView
|
||||
import dev.chrisbanes.haze.HazeState
|
||||
import dev.chrisbanes.haze.haze
|
||||
import dev.chrisbanes.haze.hazeChild
|
||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||
import dev.chrisbanes.haze.materials.HazeMaterials
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
import com.meloda.app.fast.designsystem.R as UiR
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalHazeMaterialsApi::class)
|
||||
@Composable
|
||||
fun FriendsScreen(
|
||||
onError: (BaseError) -> Unit,
|
||||
viewModel: FriendsViewModel = koinViewModel<FriendsViewModelImpl>()
|
||||
) {
|
||||
val baseError by viewModel.baseError.collectAsStateWithLifecycle()
|
||||
|
||||
val context = LocalContext.current
|
||||
|
||||
val imagesToPreload by viewModel.imagesToPreload.collectAsStateWithLifecycle()
|
||||
imagesToPreload.forEach { url ->
|
||||
context.imageLoader.enqueue(
|
||||
ImageRequest.Builder(context)
|
||||
.data(url)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
|
||||
val friends by viewModel.uiFriends.collectAsStateWithLifecycle()
|
||||
val onlineFriends by viewModel.uiOnlineFriends.collectAsStateWithLifecycle()
|
||||
val canPaginate by viewModel.canPaginate.collectAsStateWithLifecycle()
|
||||
|
||||
val currentTheme = LocalTheme.current
|
||||
|
||||
val maxLines by remember {
|
||||
derivedStateOf {
|
||||
if (currentTheme.multiline) 2 else 1
|
||||
}
|
||||
}
|
||||
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
val paginationConditionMet by remember {
|
||||
derivedStateOf {
|
||||
canPaginate &&
|
||||
(listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index
|
||||
?: -9) >= (listState.layoutInfo.totalItemsCount - 6)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(paginationConditionMet) {
|
||||
if (paginationConditionMet && !screenState.isPaginating) {
|
||||
viewModel.onMetPaginationCondition()
|
||||
}
|
||||
}
|
||||
|
||||
val hazeState = remember { HazeState() }
|
||||
|
||||
val pullToRefreshAlpha by animateFloatAsState(
|
||||
targetValue = if (!listState.canScrollBackward) 1f else 0f,
|
||||
label = "pullToRefreshAlpha",
|
||||
animationSpec = tween(durationMillis = 50)
|
||||
)
|
||||
|
||||
val tabsColorAlpha by animateFloatAsState(
|
||||
targetValue = if (!listState.canScrollBackward) 1f else 0f,
|
||||
label = "toolbarColorAlpha",
|
||||
animationSpec = tween(durationMillis = 50)
|
||||
)
|
||||
|
||||
val tabsContainerColor by animateColorAsState(
|
||||
targetValue =
|
||||
if (currentTheme.usingBlur || !listState.canScrollBackward)
|
||||
MaterialTheme.colorScheme.surface
|
||||
else
|
||||
MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp),
|
||||
label = "toolbarColorAlpha",
|
||||
animationSpec = tween(durationMillis = 50)
|
||||
)
|
||||
|
||||
Scaffold(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentWindowInsets = WindowInsets.statusBars,
|
||||
topBar = {
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
BlurrableTopAppBar(
|
||||
title = stringResource(id = UiR.string.title_friends),
|
||||
listState = listState,
|
||||
hazeState = hazeState
|
||||
)
|
||||
}
|
||||
}
|
||||
) { padding ->
|
||||
when {
|
||||
baseError is BaseError.SessionExpired -> {
|
||||
ErrorView(
|
||||
text = "Session expired",
|
||||
buttonText = "Log out",
|
||||
onButtonClick = { onError(BaseError.SessionExpired) }
|
||||
)
|
||||
}
|
||||
|
||||
screenState.isLoading && friends.isEmpty() -> FullScreenLoader()
|
||||
|
||||
else -> {
|
||||
val pullToRefreshState = rememberPullToRefreshState()
|
||||
|
||||
var selectedTabIndex by rememberSaveable {
|
||||
mutableIntStateOf(0)
|
||||
}
|
||||
|
||||
val tabItems = listOf(
|
||||
TabItem(
|
||||
titleResId = UiR.string.title_friends_all,
|
||||
unselectedIconResId = null,
|
||||
selectedIconResId = null
|
||||
),
|
||||
TabItem(
|
||||
titleResId = UiR.string.title_friends_online,
|
||||
unselectedIconResId = null,
|
||||
selectedIconResId = null
|
||||
)
|
||||
)
|
||||
|
||||
val pagerState = rememberPagerState { tabItems.size }
|
||||
|
||||
LaunchedEffect(selectedTabIndex) {
|
||||
pagerState.animateScrollToPage(selectedTabIndex)
|
||||
}
|
||||
|
||||
LaunchedEffect(pagerState) {
|
||||
snapshotFlow {
|
||||
pagerState.currentPage
|
||||
}.collect { page ->
|
||||
selectedTabIndex = page
|
||||
}
|
||||
}
|
||||
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
HorizontalPager(
|
||||
state = pagerState,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) { index ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(start = padding.calculateStartPadding(LayoutDirection.Ltr))
|
||||
.padding(end = padding.calculateEndPadding(LayoutDirection.Ltr))
|
||||
.padding(bottom = padding.calculateBottomPadding())
|
||||
.nestedScroll(pullToRefreshState.nestedScrollConnection)
|
||||
) {
|
||||
val friendsToDisplay = if (index == 0) friends
|
||||
else onlineFriends
|
||||
|
||||
FriendsList(
|
||||
modifier = if (currentTheme.usingBlur) {
|
||||
Modifier.haze(
|
||||
state = hazeState,
|
||||
style = HazeMaterials.thick()
|
||||
)
|
||||
} else {
|
||||
Modifier
|
||||
}.fillMaxSize(),
|
||||
screenState = screenState,
|
||||
uiFriends = ImmutableList.copyOf(friendsToDisplay),
|
||||
listState = listState,
|
||||
maxLines = maxLines,
|
||||
padding = padding
|
||||
)
|
||||
|
||||
if (friendsToDisplay.isEmpty()) {
|
||||
NoItemsView(
|
||||
modifier = Modifier
|
||||
.padding(padding.calculateTopPadding())
|
||||
.padding(top = 64.dp),
|
||||
customText = "No${if (index == 1) " online" else ""} friends :("
|
||||
)
|
||||
}
|
||||
|
||||
if (pullToRefreshState.isRefreshing) {
|
||||
LaunchedEffect(true) {
|
||||
viewModel.onRefresh()
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(screenState.isLoading) {
|
||||
if (!screenState.isLoading) {
|
||||
pullToRefreshState.endRefresh()
|
||||
}
|
||||
}
|
||||
|
||||
PullToRefreshContainer(
|
||||
state = pullToRefreshState,
|
||||
modifier = Modifier
|
||||
.padding(top = padding.calculateTopPadding())
|
||||
.padding(top = 46.dp)
|
||||
.alpha(pullToRefreshAlpha)
|
||||
.align(Alignment.TopCenter),
|
||||
contentColor = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
TabRow(
|
||||
selectedTabIndex = selectedTabIndex,
|
||||
modifier = Modifier
|
||||
.padding(top = padding.calculateTopPadding() - 4.dp)
|
||||
.height(56.dp)
|
||||
.then(
|
||||
if (currentTheme.usingBlur) {
|
||||
Modifier.hazeChild(
|
||||
state = hazeState,
|
||||
style = HazeMaterials.thick()
|
||||
)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
),
|
||||
containerColor = tabsContainerColor.copy(
|
||||
alpha = if (currentTheme.usingBlur) tabsColorAlpha else 1f
|
||||
),
|
||||
indicator = { tabPositions ->
|
||||
TabRowDefaults.PrimaryIndicator(
|
||||
modifier = Modifier.tabIndicatorOffset(tabPositions[selectedTabIndex]),
|
||||
width = 48.dp
|
||||
)
|
||||
}
|
||||
) {
|
||||
tabItems.forEachIndexed { index, item ->
|
||||
Tab(
|
||||
selected = index == selectedTabIndex,
|
||||
onClick = {
|
||||
if (selectedTabIndex != index) {
|
||||
selectedTabIndex = index
|
||||
}
|
||||
},
|
||||
text = {
|
||||
item.titleResId?.let { resId ->
|
||||
Text(text = stringResource(id = resId))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.meloda.app.fast.friends.util
|
||||
|
||||
import com.meloda.app.fast.common.UiImage
|
||||
import com.meloda.app.fast.data.VkMemoryCache
|
||||
import com.meloda.app.fast.friends.model.UiFriend
|
||||
import com.meloda.app.fast.model.api.domain.VkUser
|
||||
|
||||
fun VkUser.asPresentation(
|
||||
useContactNames: Boolean = false
|
||||
): UiFriend = UiFriend(
|
||||
userId = id,
|
||||
avatar = photo100?.let(UiImage::Url),
|
||||
title = if (useContactNames) {
|
||||
VkMemoryCache.getContact(id)?.name ?: fullName
|
||||
} else {
|
||||
fullName
|
||||
},
|
||||
onlineStatus = onlineStatus
|
||||
)
|
||||
Reference in New Issue
Block a user