diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkAudioData.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkAudioData.kt index 3bb2c562..0622b4dd 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkAudioData.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkAudioData.kt @@ -27,7 +27,7 @@ data class VkAudioData( @Json(name = "title") val title: String, @Json(name = "owner_id") val ownerId: Int, @Json(name = "access_key") val accessKey: String, - @Json(name = "thumb") val thumb: Thumb + @Json(name = "thumb") val thumb: Thumb? ) { @JsonClass(generateAdapter = true) diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkPhotoData.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkPhotoData.kt index 9e9c965e..1a7a0385 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkPhotoData.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkPhotoData.kt @@ -1,22 +1,22 @@ package dev.meloda.fast.model.api.data -import dev.meloda.fast.model.api.domain.VkPhotoDomain import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import dev.meloda.fast.model.api.domain.VkPhotoDomain @JsonClass(generateAdapter = true) data class VkPhotoData( @Json(name = "album_id") val albumId: Int, - val date: Int, - val id: Int, + @Json(name = "date") val date: Int?, + @Json(name = "id") val id: Int, @Json(name = "owner_id") val ownerId: Int, - @Json(name = "has_tags") val hasTags: Boolean, + @Json(name = "has_tags") val hasTags: Boolean?, @Json(name = "access_key") val accessKey: String?, - val sizes: List, - val text: String?, + @Json(name = "sizes") val sizes: List, + @Json(name = "text") val text: String?, @Json(name = "user_id") val userId: Int?, - val lat: Double?, - val long: Double?, + @Json(name = "lat") val lat: Double?, + @Json(name = "long") val long: Double?, @Json(name = "post_id") val postId: Int? ) : VkAttachmentData { @@ -33,7 +33,7 @@ data class VkPhotoData( date = date, id = id, ownerId = ownerId, - hasTags = hasTags, + hasTags = hasTags == true, accessKey = accessKey, sizes = sizes, text = text, diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkPhotoDomain.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkPhotoDomain.kt index 22cdfaa2..71ad3f5f 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkPhotoDomain.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkPhotoDomain.kt @@ -8,7 +8,7 @@ import java.util.Stack // TODO: 11/04/2024, Danil Nikolaev: review data class VkPhotoDomain( val albumId: Int, - val date: Int, + val date: Int?, val id: Int, val ownerId: Int, val hasTags: Boolean, diff --git a/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/ChatMaterialsViewModel.kt b/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/ChatMaterialsViewModel.kt index 725a650f..4758deee 100644 --- a/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/ChatMaterialsViewModel.kt +++ b/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/ChatMaterialsViewModel.kt @@ -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 } } diff --git a/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/model/UiChatMaterial.kt b/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/model/UiChatMaterial.kt index 035078db..8238eaee 100644 --- a/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/model/UiChatMaterial.kt +++ b/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/model/UiChatMaterial.kt @@ -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) } diff --git a/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/presentation/ChatMaterialsScreen.kt b/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/presentation/ChatMaterialsScreen.kt index 4abe439d..cb9e61ec 100644 --- a/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/presentation/ChatMaterialsScreen.kt +++ b/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/presentation/ChatMaterialsScreen.kt @@ -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( diff --git a/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/presentation/materials/FileMaterialsScreen.kt b/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/presentation/materials/FileMaterialsScreen.kt index 84d91ae5..9bc7d3aa 100644 --- a/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/presentation/materials/FileMaterialsScreen.kt +++ b/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/presentation/materials/FileMaterialsScreen.kt @@ -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())) } diff --git a/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/presentation/materials/LinkMaterialsScreen.kt b/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/presentation/materials/LinkMaterialsScreen.kt index c0432f1b..317b6fdb 100644 --- a/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/presentation/materials/LinkMaterialsScreen.kt +++ b/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/presentation/materials/LinkMaterialsScreen.kt @@ -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())) } diff --git a/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/presentation/materials/PhotoMaterialsScreen.kt b/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/presentation/materials/PhotoMaterialsScreen.kt index f3d3f1e2..1b06e880 100644 --- a/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/presentation/materials/PhotoMaterialsScreen.kt +++ b/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/presentation/materials/PhotoMaterialsScreen.kt @@ -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()) { diff --git a/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/presentation/materials/VideoMaterialsScreen.kt b/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/presentation/materials/VideoMaterialsScreen.kt index d939070c..58b5d651 100644 --- a/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/presentation/materials/VideoMaterialsScreen.kt +++ b/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/presentation/materials/VideoMaterialsScreen.kt @@ -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())) } diff --git a/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/util/ChatMaterialMapper.kt b/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/util/ChatMaterialMapper.kt index 8293a8fb..7a78b73d 100644 --- a/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/util/ChatMaterialMapper.kt +++ b/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/util/ChatMaterialMapper.kt @@ -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 + } } diff --git a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/presentation/ConversationsScreen.kt b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/presentation/ConversationsScreen.kt index 22942bf4..b663b6c0 100644 --- a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/presentation/ConversationsScreen.kt +++ b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/presentation/ConversationsScreen.kt @@ -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)) } diff --git a/feature/friends/src/main/kotlin/dev/meloda/fast/friends/FriendsViewModel.kt b/feature/friends/src/main/kotlin/dev/meloda/fast/friends/FriendsViewModel.kt index 0a6cdcbb..044c9935 100644 --- a/feature/friends/src/main/kotlin/dev/meloda/fast/friends/FriendsViewModel.kt +++ b/feature/friends/src/main/kotlin/dev/meloda/fast/friends/FriendsViewModel.kt @@ -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)