chat materials pagination and ui improvements
This commit is contained in:
+53
-9
@@ -9,10 +9,12 @@ import dev.meloda.fast.chatmaterials.navigation.ChatMaterials
|
||||
import dev.meloda.fast.chatmaterials.util.asPresentation
|
||||
import dev.meloda.fast.common.extensions.listenValue
|
||||
import dev.meloda.fast.common.extensions.setValue
|
||||
import dev.meloda.fast.data.State
|
||||
import dev.meloda.fast.data.processState
|
||||
import dev.meloda.fast.domain.MessagesUseCase
|
||||
import dev.meloda.fast.model.BaseError
|
||||
import dev.meloda.fast.model.api.domain.VkAttachmentHistoryMessage
|
||||
import dev.meloda.fast.network.VkErrorCode
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
@@ -60,7 +62,7 @@ class ChatMaterialsViewModelImpl(
|
||||
}
|
||||
|
||||
override fun onPaginationConditionsMet() {
|
||||
currentOffset.update { screenState.value.materials.size }
|
||||
currentOffset.setValue { old -> old + LOAD_COUNT }
|
||||
loadChatMaterials()
|
||||
}
|
||||
|
||||
@@ -86,20 +88,24 @@ class ChatMaterialsViewModelImpl(
|
||||
conversationMessageId = screenState.value.conversationMessageId
|
||||
).listenValue(viewModelScope) { state ->
|
||||
state.processState(
|
||||
error = { error ->
|
||||
|
||||
},
|
||||
error = ::handleError,
|
||||
success = { response ->
|
||||
val itemsCountSufficient = response.size == LOAD_COUNT
|
||||
canPaginate.setValue { itemsCountSufficient }
|
||||
|
||||
val paginationExhausted = !itemsCountSufficient &&
|
||||
screenState.value.materials.size >= LOAD_COUNT
|
||||
val paginationExhausted = !itemsCountSufficient
|
||||
&& screenState.value.materials.isNotEmpty()
|
||||
|
||||
val loadedMaterials = response.map(VkAttachmentHistoryMessage::asPresentation)
|
||||
val loadedMaterials = response.mapNotNull(VkAttachmentHistoryMessage::asPresentation)
|
||||
|
||||
val newState = screenState.value.copy(
|
||||
isPaginationExhausted = paginationExhausted
|
||||
isPaginationExhausted = paginationExhausted,
|
||||
conversationMessageId = if (loadedMaterials.size + offset > 200) {
|
||||
currentOffset.setValue { 0 }
|
||||
loadedMaterials.lastOrNull()?.conversationMessageId ?: -1
|
||||
} else {
|
||||
screenState.value.conversationMessageId
|
||||
}
|
||||
)
|
||||
|
||||
if (offset == 0) {
|
||||
@@ -125,7 +131,45 @@ class ChatMaterialsViewModelImpl(
|
||||
}
|
||||
}
|
||||
|
||||
private 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
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val LOAD_COUNT = 30
|
||||
const val LOAD_COUNT = 200
|
||||
}
|
||||
}
|
||||
|
||||
+13
-6
@@ -1,36 +1,43 @@
|
||||
package dev.meloda.fast.chatmaterials.model
|
||||
|
||||
sealed class UiChatMaterial {
|
||||
sealed class UiChatMaterial(
|
||||
open val conversationMessageId: Int
|
||||
) {
|
||||
|
||||
data class Photo(
|
||||
override val conversationMessageId: Int,
|
||||
val previewUrl: String
|
||||
) : UiChatMaterial()
|
||||
) : UiChatMaterial(conversationMessageId)
|
||||
|
||||
data class Video(
|
||||
override val conversationMessageId: Int,
|
||||
val previewUrl: String?,
|
||||
val title: String,
|
||||
val views: Int,
|
||||
val duration: String
|
||||
) : UiChatMaterial()
|
||||
) : UiChatMaterial(conversationMessageId)
|
||||
|
||||
data class Audio(
|
||||
override val conversationMessageId: Int,
|
||||
val previewUrl: String?,
|
||||
val title: String,
|
||||
val artist: String,
|
||||
val duration: String
|
||||
) : UiChatMaterial()
|
||||
) : UiChatMaterial(conversationMessageId)
|
||||
|
||||
data class File(
|
||||
override val conversationMessageId: Int,
|
||||
val previewUrl: String?,
|
||||
val title: String,
|
||||
val size: String,
|
||||
val extension: String
|
||||
) : UiChatMaterial()
|
||||
) : UiChatMaterial(conversationMessageId)
|
||||
|
||||
data class Link(
|
||||
override val conversationMessageId: Int,
|
||||
val previewUrl: String?,
|
||||
val title: String?,
|
||||
val url: String,
|
||||
val urlFirstChar: String
|
||||
) : UiChatMaterial()
|
||||
) : UiChatMaterial(conversationMessageId)
|
||||
}
|
||||
|
||||
+12
-3
@@ -17,9 +17,11 @@ import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.PrimaryTabRow
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.ScrollableTabRow
|
||||
import androidx.compose.material3.Tab
|
||||
import androidx.compose.material3.TabRowDefaults
|
||||
import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
@@ -171,10 +173,17 @@ fun ChatMaterialsScreen(
|
||||
}
|
||||
}
|
||||
)
|
||||
PrimaryTabRow(
|
||||
ScrollableTabRow(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
selectedTabIndex = selectedTabIndex,
|
||||
containerColor = Color.Transparent
|
||||
containerColor = Color.Transparent,
|
||||
edgePadding = 0.dp,
|
||||
indicator = { tabPositions ->
|
||||
TabRowDefaults.PrimaryIndicator(
|
||||
modifier = Modifier.tabIndicatorOffset(tabPositions[selectedTabIndex]),
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
) {
|
||||
tabItems.forEachIndexed { index, item ->
|
||||
Tab(
|
||||
|
||||
+56
@@ -19,7 +19,13 @@ import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.KeyboardArrowUp
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.IconButtonDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||
@@ -28,9 +34,11 @@ 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.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
@@ -60,6 +68,8 @@ import dev.meloda.fast.ui.components.FullScreenLoader
|
||||
import dev.meloda.fast.ui.components.NoItemsView
|
||||
import dev.meloda.fast.ui.theme.LocalHazeState
|
||||
import dev.meloda.fast.ui.theme.LocalThemeConfig
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -74,6 +84,7 @@ fun FileMaterialsScreen(
|
||||
setCanScrollBackward: (Boolean) -> Unit,
|
||||
onPaginationConditionsMet: () -> Unit
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val hazeState = LocalHazeState.current
|
||||
val currentTheme = LocalThemeConfig.current
|
||||
val listState = rememberLazyListState()
|
||||
@@ -84,6 +95,20 @@ fun FileMaterialsScreen(
|
||||
.collect(setCanScrollBackward)
|
||||
}
|
||||
|
||||
val paginationConditionMet by remember(canPaginate, listState) {
|
||||
derivedStateOf {
|
||||
canPaginate &&
|
||||
(listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index
|
||||
?: -9) >= (listState.layoutInfo.totalItemsCount - 6)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(paginationConditionMet) {
|
||||
if (paginationConditionMet && !screenState.isPaginating) {
|
||||
onPaginationConditionsMet()
|
||||
}
|
||||
}
|
||||
|
||||
when {
|
||||
baseError != null -> {
|
||||
when (baseError) {
|
||||
@@ -207,6 +232,37 @@ fun FileMaterialsScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
item {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.animateItem(fadeInSpec = null, fadeOutSpec = null),
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
}
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(padding.calculateBottomPadding()))
|
||||
}
|
||||
|
||||
+56
@@ -19,7 +19,13 @@ import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.KeyboardArrowUp
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.IconButtonDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||
@@ -28,9 +34,11 @@ 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.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
@@ -60,6 +68,8 @@ import dev.meloda.fast.ui.components.FullScreenLoader
|
||||
import dev.meloda.fast.ui.components.NoItemsView
|
||||
import dev.meloda.fast.ui.theme.LocalHazeState
|
||||
import dev.meloda.fast.ui.theme.LocalThemeConfig
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -74,6 +84,7 @@ fun LinkMaterialsScreen(
|
||||
setCanScrollBackward: (Boolean) -> Unit,
|
||||
onPaginationConditionsMet: () -> Unit
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val hazeState = LocalHazeState.current
|
||||
val currentTheme = LocalThemeConfig.current
|
||||
val listState = rememberLazyListState()
|
||||
@@ -84,6 +95,20 @@ fun LinkMaterialsScreen(
|
||||
.collect(setCanScrollBackward)
|
||||
}
|
||||
|
||||
val paginationConditionMet by remember(canPaginate, listState) {
|
||||
derivedStateOf {
|
||||
canPaginate &&
|
||||
(listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index
|
||||
?: -9) >= (listState.layoutInfo.totalItemsCount - 6)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(paginationConditionMet) {
|
||||
if (paginationConditionMet && !screenState.isPaginating) {
|
||||
onPaginationConditionsMet()
|
||||
}
|
||||
}
|
||||
|
||||
when {
|
||||
baseError != null -> {
|
||||
when (baseError) {
|
||||
@@ -226,6 +251,37 @@ fun LinkMaterialsScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
item {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.animateItem(fadeInSpec = null, fadeOutSpec = null),
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
}
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(padding.calculateBottomPadding()))
|
||||
}
|
||||
|
||||
+69
-7
@@ -2,6 +2,7 @@ package dev.meloda.fast.chatmaterials.presentation.materials
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
@@ -12,15 +13,26 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.GridItemSpan
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.lazy.grid.items
|
||||
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.KeyboardArrowUp
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.IconButtonDefaults
|
||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||
import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults
|
||||
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -39,6 +51,8 @@ import dev.meloda.fast.ui.components.FullScreenLoader
|
||||
import dev.meloda.fast.ui.components.NoItemsView
|
||||
import dev.meloda.fast.ui.theme.LocalHazeState
|
||||
import dev.meloda.fast.ui.theme.LocalThemeConfig
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -54,6 +68,7 @@ fun PhotoMaterialsScreen(
|
||||
onPhotoClicked: (String) -> Unit,
|
||||
onPaginationConditionsMet: () -> Unit
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val hazeState = LocalHazeState.current
|
||||
val currentTheme = LocalThemeConfig.current
|
||||
val gridState = rememberLazyGridState()
|
||||
@@ -64,6 +79,20 @@ fun PhotoMaterialsScreen(
|
||||
.collect(setCanScrollBackward)
|
||||
}
|
||||
|
||||
val paginationConditionMet by remember(canPaginate, gridState) {
|
||||
derivedStateOf {
|
||||
canPaginate &&
|
||||
(gridState.layoutInfo.visibleItemsInfo.lastOrNull()?.index
|
||||
?: -9) >= (gridState.layoutInfo.totalItemsCount - 6)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(paginationConditionMet) {
|
||||
if (paginationConditionMet && !screenState.isPaginating) {
|
||||
onPaginationConditionsMet()
|
||||
}
|
||||
}
|
||||
|
||||
when {
|
||||
baseError != null -> {
|
||||
when (baseError) {
|
||||
@@ -121,10 +150,8 @@ fun PhotoMaterialsScreen(
|
||||
.fillMaxSize()
|
||||
|
||||
) {
|
||||
repeat(3) {
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(padding.calculateTopPadding()))
|
||||
}
|
||||
item(span = { GridItemSpan(3) }) {
|
||||
Spacer(modifier = Modifier.height(padding.calculateTopPadding()))
|
||||
}
|
||||
items(items = screenState.materials) { item ->
|
||||
item as UiChatMaterial.Photo
|
||||
@@ -142,11 +169,46 @@ fun PhotoMaterialsScreen(
|
||||
)
|
||||
)
|
||||
}
|
||||
repeat(3) {
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(padding.calculateBottomPadding()))
|
||||
item(span = { GridItemSpan(3) }) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.animateItem(fadeInSpec = null, fadeOutSpec = null),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
if (screenState.isPaginating) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
|
||||
if (screenState.isPaginationExhausted) {
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
|
||||
IconButton(
|
||||
onClick = {
|
||||
coroutineScope.launch(Dispatchers.Main) {
|
||||
gridState.scrollToItem(14)
|
||||
gridState.animateScrollToItem(0)
|
||||
}
|
||||
},
|
||||
colors = IconButtonDefaults.filledIconButtonColors()
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.KeyboardArrowUp,
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
}
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(padding.calculateBottomPadding()))
|
||||
}
|
||||
item(span = { GridItemSpan(3) }) {
|
||||
Spacer(modifier = Modifier.height(padding.calculateBottomPadding()))
|
||||
}
|
||||
}
|
||||
|
||||
if (screenState.materials.isEmpty()) {
|
||||
|
||||
+58
@@ -19,7 +19,13 @@ import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.KeyboardArrowUp
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.IconButtonDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||
@@ -27,6 +33,10 @@ import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults
|
||||
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -51,6 +61,8 @@ import dev.meloda.fast.ui.components.FullScreenLoader
|
||||
import dev.meloda.fast.ui.components.NoItemsView
|
||||
import dev.meloda.fast.ui.theme.LocalHazeState
|
||||
import dev.meloda.fast.ui.theme.LocalThemeConfig
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -65,6 +77,7 @@ fun VideoMaterialsScreen(
|
||||
setCanScrollBackward: (Boolean) -> Unit,
|
||||
onPaginationConditionsMet: () -> Unit
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val hazeState = LocalHazeState.current
|
||||
val currentTheme = LocalThemeConfig.current
|
||||
val listState = rememberLazyListState()
|
||||
@@ -75,6 +88,20 @@ fun VideoMaterialsScreen(
|
||||
.collect(setCanScrollBackward)
|
||||
}
|
||||
|
||||
val paginationConditionMet by remember(canPaginate, listState) {
|
||||
derivedStateOf {
|
||||
canPaginate &&
|
||||
(listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index
|
||||
?: -9) >= (listState.layoutInfo.totalItemsCount - 6)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(paginationConditionMet) {
|
||||
if (paginationConditionMet && !screenState.isPaginating) {
|
||||
onPaginationConditionsMet()
|
||||
}
|
||||
}
|
||||
|
||||
when {
|
||||
baseError != null -> {
|
||||
when (baseError) {
|
||||
@@ -197,6 +224,37 @@ fun VideoMaterialsScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
item {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.animateItem(fadeInSpec = null, fadeOutSpec = null),
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
}
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(padding.calculateBottomPadding()))
|
||||
}
|
||||
|
||||
+11
-2
@@ -1,5 +1,6 @@
|
||||
package dev.meloda.fast.chatmaterials.util
|
||||
|
||||
import android.util.Log
|
||||
import dev.meloda.fast.chatmaterials.model.UiChatMaterial
|
||||
import dev.meloda.fast.common.util.AndroidUtils
|
||||
import dev.meloda.fast.model.api.data.AttachmentType
|
||||
@@ -11,11 +12,12 @@ import dev.meloda.fast.model.api.domain.VkPhotoDomain
|
||||
import dev.meloda.fast.model.api.domain.VkVideoDomain
|
||||
import java.util.Locale
|
||||
|
||||
fun VkAttachmentHistoryMessage.asPresentation(): UiChatMaterial =
|
||||
fun VkAttachmentHistoryMessage.asPresentation(): UiChatMaterial? =
|
||||
when (val type = this.attachment.type) {
|
||||
AttachmentType.PHOTO -> {
|
||||
val attachment = this.attachment as VkPhotoDomain
|
||||
UiChatMaterial.Photo(
|
||||
conversationMessageId = this.conversationMessageId,
|
||||
previewUrl = attachment.getSizeOrSmaller(VkPhotoDomain.SIZE_TYPE_1080_1024)?.url.orEmpty()
|
||||
)
|
||||
}
|
||||
@@ -45,6 +47,7 @@ fun VkAttachmentHistoryMessage.asPresentation(): UiChatMaterial =
|
||||
builder.toString().format(Locale.getDefault(), *args.toTypedArray())
|
||||
|
||||
UiChatMaterial.Video(
|
||||
conversationMessageId = this.conversationMessageId,
|
||||
previewUrl = attachment.images.maxByOrNull(VkVideoDomain.VideoImage::width)?.url.orEmpty(),
|
||||
title = attachment.title,
|
||||
views = attachment.views,
|
||||
@@ -77,6 +80,7 @@ fun VkAttachmentHistoryMessage.asPresentation(): UiChatMaterial =
|
||||
builder.toString().format(Locale.getDefault(), *args.toTypedArray())
|
||||
|
||||
UiChatMaterial.Audio(
|
||||
conversationMessageId = this.conversationMessageId,
|
||||
previewUrl = null,
|
||||
title = attachment.title,
|
||||
artist = attachment.artist,
|
||||
@@ -108,6 +112,7 @@ fun VkAttachmentHistoryMessage.asPresentation(): UiChatMaterial =
|
||||
}
|
||||
|
||||
UiChatMaterial.File(
|
||||
conversationMessageId = this.conversationMessageId,
|
||||
title = attachment.title,
|
||||
previewUrl = previewUrl,
|
||||
size = AndroidUtils.bytesToHumanReadableSize(attachment.size.toDouble()),
|
||||
@@ -119,6 +124,7 @@ fun VkAttachmentHistoryMessage.asPresentation(): UiChatMaterial =
|
||||
val attachment = this.attachment as VkLinkDomain
|
||||
|
||||
UiChatMaterial.Link(
|
||||
conversationMessageId = this.conversationMessageId,
|
||||
title = attachment.title,
|
||||
previewUrl = attachment.photo?.getMaxSize()?.url,
|
||||
url = attachment.url,
|
||||
@@ -129,5 +135,8 @@ fun VkAttachmentHistoryMessage.asPresentation(): UiChatMaterial =
|
||||
)
|
||||
}
|
||||
|
||||
else -> throw IllegalArgumentException("Unsupported type: $type")
|
||||
else -> {
|
||||
Log.w("ChatMaterialMapper", "Unsupported type: $type")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
+18
-8
@@ -3,6 +3,7 @@ package dev.meloda.fast.conversations.presentation
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.animateIntAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
@@ -17,6 +18,7 @@ import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.statusBars
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
@@ -51,7 +53,6 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
@@ -277,19 +278,28 @@ fun ConversationsScreen(
|
||||
}
|
||||
},
|
||||
floatingActionButton = {
|
||||
val offsetY by animateIntAsState(
|
||||
targetValue = if (listState.isScrollingUp()) 0 else 600
|
||||
)
|
||||
|
||||
Column {
|
||||
AnimatedVisibility(
|
||||
visible = listState.isScrollingUp(),
|
||||
enter = slideIn { IntOffset(0, 600) } + fadeIn(tween(200)),
|
||||
exit = slideOut { IntOffset(0, 600) } + fadeOut(tween(200))
|
||||
) {
|
||||
FloatingActionButton(onClick = onCreateChatButtonClicked) {
|
||||
// AnimatedVisibility(
|
||||
// visible = listState.isScrollingUp(),
|
||||
// enter = slideIn { IntOffset(0, 600) } + fadeIn(tween(200)),
|
||||
// exit = slideOut { IntOffset(0, 600) } + fadeOut(tween(200))
|
||||
// ) {
|
||||
FloatingActionButton(
|
||||
onClick = onCreateChatButtonClicked,
|
||||
modifier = Modifier.offset {
|
||||
IntOffset(0, offsetY)
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = UiR.drawable.ic_baseline_create_24),
|
||||
contentDescription = "Add chat button"
|
||||
)
|
||||
}
|
||||
}
|
||||
// }
|
||||
|
||||
Spacer(modifier = Modifier.height(LocalBottomPadding.current))
|
||||
}
|
||||
|
||||
@@ -157,8 +157,8 @@ class FriendsViewModelImpl(
|
||||
val itemsCountSufficient = response.size == LOAD_COUNT
|
||||
canPaginate.setValue { itemsCountSufficient }
|
||||
|
||||
val paginationExhausted = !itemsCountSufficient &&
|
||||
screenState.value.friends.size >= LOAD_COUNT
|
||||
val paginationExhausted = !itemsCountSufficient
|
||||
&& screenState.value.friends.isNotEmpty()
|
||||
|
||||
imagesToPreload.setValue {
|
||||
response.mapNotNull(VkUser::photo100)
|
||||
|
||||
Reference in New Issue
Block a user