reworked chat materials screen and some fixes

This commit is contained in:
2025-03-27 02:27:19 +03:00
parent 37a654790c
commit 807c23926e
22 changed files with 1566 additions and 363 deletions
@@ -1,75 +1,78 @@
package dev.meloda.fast.model.api.data
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import dev.meloda.fast.model.api.domain.VkVideoDomain
@JsonClass(generateAdapter = true)
data class VkVideoData(
val id: Int,
val title: String,
val width: Int?,
val height: Int?,
val duration: Int,
val date: Int,
val comments: Int?,
val description: String?,
val player: String?,
val added: Int?,
val type: String,
val views: Int,
val access_key: String?,
val owner_id: Int,
val is_favorite: Boolean?,
val image: List<Image>?,
val first_frame: List<FirstFrame>?,
val files: File?
@Json(name = "id") val id: Int,
@Json(name = "title") val title: String,
@Json(name = "width") val width: Int?,
@Json(name = "height") val height: Int?,
@Json(name = "duration") val duration: Int,
@Json(name = "date") val date: Int,
@Json(name = "comments") val comments: Int?,
@Json(name = "description") val description: String?,
@Json(name = "player") val player: String?,
@Json(name = "added") val added: Int?,
@Json(name = "type") val type: String,
@Json(name = "views") val views: Int,
@Json(name = "access_key") val accessKey: String?,
@Json(name = "owner_id") val ownerId: Int,
@Json(name = "is_favorite") val isFavorite: Boolean?,
@Json(name = "image") val image: List<Image>?,
@Json(name = "first_frame") val firstFrame: List<FirstFrame>?,
@Json(name = "files") val files: File?
) : VkAttachmentData {
@JsonClass(generateAdapter = true)
data class Image(
val width: Int,
val height: Int,
val url: String,
val with_padding: Int?
@Json(name = "width") val width: Int,
@Json(name = "height") val height: Int,
@Json(name = "url") val url: String,
@Json(name = "with_padding") val withPadding: Int?
) {
fun asVideoImage() = VkVideoDomain.VideoImage(
width = width,
height = height,
url = url,
withPadding = with_padding == 1
withPadding = withPadding == 1
)
}
@JsonClass(generateAdapter = true)
data class FirstFrame(
val height: Int,
val width: Int,
val url: String
@Json(name = "height") val height: Int,
@Json(name = "width") val width: Int,
@Json(name = "url") val url: String
)
@JsonClass(generateAdapter = true)
data class File(
val mp4_240: String?,
val mp4_360: String?,
val mp4_480: String?,
val mp4_720: String?,
val mp4_1080: String?,
val mp4_1440: String?,
val hls: String?,
val dash_uni: String?,
val dash_sep: String?,
val hls_ondemand: String?,
val dash_ondemand: String?,
val failover_host: String?
@Json(name = "mp4_240") val mp4240: String?,
@Json(name = "mp4_360") val mp4360: String?,
@Json(name = "mp4_480") val mp4480: String?,
@Json(name = "mp4_720") val mp4720: String?,
@Json(name = "mp4_1080") val mp41080: String?,
@Json(name = "mp4_1440") val mp41440: String?,
@Json(name = "hls") val hls: String?,
@Json(name = "dash_uni") val dashUni: String?,
@Json(name = "dash_sep") val dashSep: String?,
@Json(name = "hls_ondemand") val hlsOnDemand: String?,
@Json(name = "dash_ondemand") val dashOnDemand: String?,
@Json(name = "failover_host") val failOverHost: String?
)
fun toDomain() = VkVideoDomain(
id = id,
ownerId = owner_id,
ownerId = ownerId,
images = image.orEmpty().map { it.asVideoImage() },
firstFrames = first_frame,
accessKey = access_key,
title = title
firstFrames = firstFrame,
accessKey = accessKey,
title = title,
views = views,
duration = duration
)
}
@@ -12,6 +12,8 @@ data class VkVideoDomain(
val firstFrames: List<VkVideoData.FirstFrame>?,
val accessKey: String?,
val title: String,
val views: Int,
val duration: Int
) : VkAttachment {
override val type: AttachmentType = AttachmentType.VIDEO
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#ffffff"
android:pathData="M9 13V5C9 3.9 9.9 3 11 3H20C21.1 3 22 3.9 22 5V11H18.57L17.29 9.26C17.23 9.17 17.11 9.17 17.05 9.26L15.06 12C15 12.06 14.88 12.07 14.82 12L13.39 10.25C13.33 10.18 13.22 10.18 13.16 10.25L11.05 12.91C10.97 13 11.04 13.15 11.16 13.15H17.5V15H11C9.89 15 9 14.11 9 13M6 22V21H4V22H2V2H4V3H6V2H8.39C7.54 2.74 7 3.8 7 5V13C7 15.21 8.79 17 11 17H15.7C14.67 17.83 14 19.08 14 20.5C14 21.03 14.11 21.53 14.28 22H6M4 7H6V5H4V7M4 11H6V9H4V11M4 15H6V13H4V15M6 19V17H4V19H6M23 13V15H21V20.5C21 21.88 19.88 23 18.5 23S16 21.88 16 20.5 17.12 18 18.5 18C18.86 18 19.19 18.07 19.5 18.21V13H23Z" />
</vector>
@@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M8,6.82v10.36c0,0.79 0.87,1.27 1.54,0.84l8.14,-5.18c0.62,-0.39 0.62,-1.29 0,-1.69L9.54,5.98C8.87,5.55 8,6.03 8,6.82z" />
</vector>
@@ -218,4 +218,15 @@
<string name="title_create_chat">Создать чат</string>
<string name="action_create">Создать</string>
<string name="create_chat_title">Название</string>
<string name="chat_materials_title">Вложения чата</string>
<string name="chat_materials_action_title">Вложения</string>
<string name="friends_order_priority">Приоритет</string>
<string name="friends_order_name">Имя</string>
<string name="friends_order_random">Случайно</string>
<string name="friends_order_by_title">Упорядочить по</string>
<string name="chat_attachment_photos">Фото</string>
<string name="chat_attachment_videos">Видео</string>
<string name="chat_attachment_music">Музыка</string>
<string name="chat_attachment_files">Файлы</string>
<string name="chat_attachment_links">Ссылки</string>
</resources>
+13
View File
@@ -283,4 +283,17 @@
<string name="title_create_chat">Create chat</string>
<string name="action_create">Create</string>
<string name="create_chat_title">Title</string>
<string name="chat_materials_title">Chat materials</string>
<string name="chat_materials_action_title">Materials</string>
<string name="friends_order_priority">Priority</string>
<string name="friends_order_name">Name</string>
<string name="friends_order_random">Random</string>
<string name="friends_order_mobile" translatable="false">Mobile</string>
<string name="friends_order_smart" translatable="false">Smart</string>
<string name="friends_order_by_title">Order by</string>
<string name="chat_attachment_photos">Photos</string>
<string name="chat_attachment_videos">Videos</string>
<string name="chat_attachment_music">Music</string>
<string name="chat_attachment_files">Files</string>
<string name="chat_attachment_links">Links</string>
</resources>
@@ -4,6 +4,7 @@ import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dev.meloda.fast.chatmaterials.model.ChatMaterialsScreenState
import dev.meloda.fast.chatmaterials.model.MaterialType
import dev.meloda.fast.chatmaterials.navigation.ChatMaterials
import dev.meloda.fast.chatmaterials.util.asPresentation
import dev.meloda.fast.common.extensions.listenValue
@@ -23,7 +24,7 @@ interface ChatMaterialsViewModel {
val currentOffset: StateFlow<Int>
val canPaginate: StateFlow<Boolean>
fun onMetPaginationCondition()
fun onPaginationConditionsMet()
fun onRefresh()
@@ -33,6 +34,7 @@ interface ChatMaterialsViewModel {
}
class ChatMaterialsViewModelImpl(
private val materialType: MaterialType,
private val messagesUseCase: MessagesUseCase,
savedStateHandle: SavedStateHandle
) : ViewModel(), ChatMaterialsViewModel {
@@ -57,7 +59,7 @@ class ChatMaterialsViewModelImpl(
loadChatMaterials()
}
override fun onMetPaginationCondition() {
override fun onPaginationConditionsMet() {
currentOffset.update { screenState.value.materials.size }
loadChatMaterials()
}
@@ -75,14 +77,12 @@ class ChatMaterialsViewModelImpl(
loadChatMaterials(0)
}
private fun loadChatMaterials(
offset: Int = currentOffset.value
) {
private fun loadChatMaterials(offset: Int = currentOffset.value) {
messagesUseCase.getHistoryAttachments(
peerId = screenState.value.peerId,
count = LOAD_COUNT,
offset = offset,
attachmentTypes = listOf(screenState.value.attachmentType),
attachmentTypes = listOf(materialType.toString()),
conversationMessageId = screenState.value.conversationMessageId
).listenValue(viewModelScope) { state ->
state.processState(
@@ -126,6 +126,6 @@ class ChatMaterialsViewModelImpl(
}
companion object {
const val LOAD_COUNT = 100
const val LOAD_COUNT = 30
}
}
@@ -1,9 +1,45 @@
package dev.meloda.fast.chatmaterials.di
import dev.meloda.fast.chatmaterials.ChatMaterialsViewModelImpl
import org.koin.androidx.viewmodel.dsl.viewModelOf
import dev.meloda.fast.chatmaterials.model.MaterialType
import org.koin.core.module.dsl.viewModel
import org.koin.core.qualifier.named
import org.koin.dsl.module
val chatMaterialsModule = module {
viewModelOf(::ChatMaterialsViewModelImpl)
viewModel(named(MaterialType.PHOTO)) {
ChatMaterialsViewModelImpl(
materialType = MaterialType.PHOTO,
messagesUseCase = get(),
savedStateHandle = get()
)
}
viewModel(named(MaterialType.AUDIO)) {
ChatMaterialsViewModelImpl(
materialType = MaterialType.AUDIO,
messagesUseCase = get(),
savedStateHandle = get()
)
}
viewModel(named(MaterialType.VIDEO)) {
ChatMaterialsViewModelImpl(
materialType = MaterialType.VIDEO,
messagesUseCase = get(),
savedStateHandle = get()
)
}
viewModel(named(MaterialType.FILE)) {
ChatMaterialsViewModelImpl(
materialType = MaterialType.FILE,
messagesUseCase = get(),
savedStateHandle = get()
)
}
viewModel(named(MaterialType.LINK)) {
ChatMaterialsViewModelImpl(
materialType = MaterialType.LINK,
messagesUseCase = get(),
savedStateHandle = get()
)
}
}
@@ -0,0 +1,13 @@
package dev.meloda.fast.chatmaterials.model
enum class MaterialType {
PHOTO, VIDEO, AUDIO, FILE, LINK;
override fun toString(): String = when (this) {
PHOTO -> "photo"
VIDEO -> "video"
AUDIO -> "audio"
FILE -> "doc"
LINK -> "link"
}
}
@@ -7,7 +7,10 @@ sealed class UiChatMaterial {
) : UiChatMaterial()
data class Video(
val previewUrl: String
val previewUrl: String?,
val title: String,
val views: Int,
val duration: String
) : UiChatMaterial()
data class Audio(
@@ -18,11 +21,16 @@ sealed class UiChatMaterial {
) : UiChatMaterial()
data class File(
val title: String
val previewUrl: String?,
val title: String,
val size: String,
val extension: String
) : UiChatMaterial()
data class Link(
val title: String,
val previewUrl: String?
val previewUrl: String?,
val title: String?,
val url: String,
val urlFirstChar: String
) : UiChatMaterial()
}
@@ -1,93 +1,69 @@
package dev.meloda.fast.chatmaterials.presentation
import android.annotation.SuppressLint
import android.util.Log
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.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.calculateEndPadding
import androidx.compose.foundation.layout.calculateStartPadding
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.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material.icons.outlined.MoreVert
import androidx.compose.material.icons.rounded.Refresh
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
import androidx.compose.material3.PrimaryTabRow
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Tab
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
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.DpOffset
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import dev.chrisbanes.haze.HazeState
import dev.chrisbanes.haze.hazeEffect
import dev.chrisbanes.haze.hazeSource
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
import dev.chrisbanes.haze.materials.HazeMaterials
import dev.meloda.fast.chatmaterials.ChatMaterialsViewModel
import dev.meloda.fast.chatmaterials.ChatMaterialsViewModelImpl
import dev.meloda.fast.chatmaterials.model.ChatMaterialsScreenState
import dev.meloda.fast.chatmaterials.model.UiChatMaterial
import dev.meloda.fast.ui.R
import dev.meloda.fast.chatmaterials.model.MaterialType
import dev.meloda.fast.chatmaterials.presentation.materials.AudioMaterialsScreen
import dev.meloda.fast.chatmaterials.presentation.materials.FileMaterialsScreen
import dev.meloda.fast.chatmaterials.presentation.materials.LinkMaterialsScreen
import dev.meloda.fast.chatmaterials.presentation.materials.PhotoMaterialsScreen
import dev.meloda.fast.chatmaterials.presentation.materials.VideoMaterialsScreen
import dev.meloda.fast.ui.model.TabItem
import dev.meloda.fast.ui.theme.LocalThemeConfig
import kotlinx.coroutines.launch
import org.koin.androidx.compose.koinViewModel
import org.koin.core.qualifier.named
import dev.meloda.fast.ui.R as UiR
@Composable
fun ChatMaterialsRoute(
onBack: () -> Unit,
onPhotoClicked: (url: String) -> Unit,
viewModel: ChatMaterialsViewModel = koinViewModel<ChatMaterialsViewModelImpl>()
) {
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
ChatMaterialsScreen(
screenState = screenState,
onBack = onBack,
onTypeChanged = viewModel::onTypeChanged,
onRefreshDropdownItemClicked = viewModel::onRefresh,
onRefresh = viewModel::onRefresh,
onPhotoClicked = onPhotoClicked
)
}
@@ -99,55 +75,36 @@ fun ChatMaterialsRoute(
)
@Composable
fun ChatMaterialsScreen(
screenState: ChatMaterialsScreenState = ChatMaterialsScreenState.EMPTY,
onBack: () -> Unit = {},
onTypeChanged: (String) -> Unit = {},
onRefreshDropdownItemClicked: () -> Unit = {},
onRefresh: () -> Unit = {},
onPhotoClicked: (url: String) -> Unit = {}
) {
val scope = rememberCoroutineScope()
val currentTheme = LocalThemeConfig.current
val attachments = screenState.materials
var moreClearBlur by rememberSaveable {
mutableStateOf(false)
}
val hazeState = remember { HazeState() }
val hazeStyle = if (moreClearBlur) HazeMaterials.ultraThin() else HazeMaterials.regular()
var dropDownMenuExpanded by remember {
mutableStateOf(false)
}
var checkedTypeIndex by rememberSaveable {
mutableIntStateOf(0)
}
LaunchedEffect(checkedTypeIndex) {
onTypeChanged(
when (checkedTypeIndex) {
0 -> "photo"
1 -> "video"
2 -> "audio"
3 -> "doc"
4 -> "link"
else -> ""
}
val titles = remember {
listOf(
UiR.string.chat_attachment_photos,
UiR.string.chat_attachment_videos,
UiR.string.chat_attachment_music,
UiR.string.chat_attachment_files,
UiR.string.chat_attachment_links,
)
}
val titles = listOf("Photos", "Videos", "Audios")//, "Files", "Links")
val listState = rememberLazyListState()
val gridState = rememberLazyGridState()
val canScrollBackward = when (checkedTypeIndex) {
in 0..1 -> gridState.canScrollBackward
else -> listState.canScrollBackward
val tabItems = remember {
titles.map { resId ->
TabItem(
titleResId = resId,
unselectedIconResId = null,
selectedIconResId = null
)
}
}
Log.d("ChatMaterialsScreen", "ChatMaterialsScreen: canScrollBackward: $canScrollBackward")
var canScrollBackward by remember {
mutableStateOf(false)
}
val topBarContainerColorAlpha by animateFloatAsState(
targetValue = if (!currentTheme.enableBlur || !canScrollBackward) 1f else 0f,
@@ -160,10 +117,8 @@ fun ChatMaterialsScreen(
val topBarContainerColor by animateColorAsState(
targetValue =
if (currentTheme.enableBlur || !canScrollBackward)
MaterialTheme.colorScheme.surface
else
MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp),
if (currentTheme.enableBlur || !canScrollBackward) MaterialTheme.colorScheme.surface
else MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp),
label = "toolbarColorAlpha",
animationSpec = tween(
durationMillis = 200,
@@ -171,7 +126,13 @@ fun ChatMaterialsScreen(
)
)
val pullToRefreshState = rememberPullToRefreshState()
val pagerState = rememberPagerState(
pageCount = tabItems::size
)
val selectedTabIndex by remember {
derivedStateOf { pagerState.currentPage }
}
Scaffold(
topBar = {
@@ -181,11 +142,9 @@ fun ChatMaterialsScreen(
if (currentTheme.enableBlur) {
Modifier.hazeEffect(
state = hazeState,
style = hazeStyle
style = HazeMaterials.thick()
)
} else {
Modifier
}
} else Modifier
)
.background(topBarContainerColor.copy(alpha = topBarContainerColorAlpha))
.fillMaxWidth()
@@ -193,7 +152,7 @@ fun ChatMaterialsScreen(
TopAppBar(
title = {
Text(
text = "Chat Materials",
text = stringResource(UiR.string.chat_materials_title),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.headlineSmall
@@ -210,163 +169,139 @@ fun ChatMaterialsScreen(
contentDescription = null
)
}
},
actions = {
IconButton(
onClick = {
dropDownMenuExpanded = true
}
)
PrimaryTabRow(
modifier = Modifier.fillMaxWidth(),
selectedTabIndex = selectedTabIndex,
containerColor = Color.Transparent
) {
Icon(
imageVector = Icons.Outlined.MoreVert,
contentDescription = "Options button"
)
}
DropdownMenu(
modifier = Modifier.defaultMinSize(minWidth = 140.dp),
expanded = dropDownMenuExpanded,
onDismissRequest = {
dropDownMenuExpanded = false
},
offset = DpOffset(x = (-4).dp, y = (-60).dp)
) {
DropdownMenuItem(
tabItems.forEachIndexed { index, item ->
Tab(
selected = index == selectedTabIndex,
onClick = {
onRefreshDropdownItemClicked()
dropDownMenuExpanded = false
scope.launch {
pagerState.animateScrollToPage(index)
}
},
text = {
Text(text = stringResource(id = R.string.action_refresh))
},
leadingIcon = {
Icon(
imageVector = Icons.Rounded.Refresh,
contentDescription = null
)
}
)
if (currentTheme.enableBlur) {
DropdownMenuItem(
text = {
Text(text = if (moreClearBlur) "Default blur" else "Clearer blur")
},
onClick = {
moreClearBlur = !moreClearBlur
dropDownMenuExpanded = false
}
)
}
HorizontalDivider()
titles.forEachIndexed { index, title ->
DropdownMenuItem(
leadingIcon = {
RadioButton(
selected = checkedTypeIndex == index,
onClick = null
)
},
text = {
Text(text = title)
},
onClick = {
checkedTypeIndex = index
dropDownMenuExpanded = false
}
)
}
item.titleResId?.let { resId ->
Text(text = stringResource(id = resId))
}
}
)
}
}
}
}
) { padding ->
PullToRefreshBox(
modifier = Modifier
.fillMaxSize()
.padding(start = padding.calculateStartPadding(LayoutDirection.Ltr))
.padding(end = padding.calculateEndPadding(LayoutDirection.Ltr)),
state = pullToRefreshState,
isRefreshing = screenState.isLoading,
onRefresh = onRefresh,
indicator = {
PullToRefreshDefaults.Indicator(
state = pullToRefreshState,
isRefreshing = screenState.isLoading,
modifier = Modifier
.align(Alignment.TopCenter)
.padding(top = padding.calculateTopPadding()),
)
}
) {
if (checkedTypeIndex in listOf(0, 1)) {
LazyVerticalGrid(
columns = GridCells.Fixed(3),
state = gridState,
horizontalArrangement = Arrangement.spacedBy(2.dp),
modifier = Modifier
.then(
if (currentTheme.enableBlur) {
Modifier.hazeSource(state = hazeState)
} else {
Modifier
}
)
.fillMaxSize()
HorizontalPager(
state = pagerState,
modifier = Modifier.fillMaxSize(),
) { index ->
when (index) {
0 -> {
val viewModel: ChatMaterialsViewModel =
koinViewModel<ChatMaterialsViewModelImpl>(named(MaterialType.PHOTO))
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
val baseError by viewModel.baseError.collectAsStateWithLifecycle()
val canPaginate by viewModel.canPaginate.collectAsStateWithLifecycle()
) {
repeat(3) {
item {
Spacer(modifier = Modifier.height(padding.calculateTopPadding()))
}
}
items(attachments) { item ->
ChatMaterialItem(
item = item,
onClick = {
if (item is UiChatMaterial.Photo) {
onPhotoClicked(item.previewUrl)
}
}
PhotoMaterialsScreen(
modifier = Modifier,
screenState = screenState,
baseError = baseError,
padding = padding,
onRefresh = viewModel::onRefresh,
onSessionExpiredLogOutButtonClicked = { },
setCanScrollBackward = { canScrollBackward = it },
canPaginate = canPaginate,
onPaginationConditionsMet = viewModel::onPaginationConditionsMet,
onPhotoClicked = onPhotoClicked
)
}
repeat(3) {
item {
Spacer(modifier = Modifier.height(padding.calculateBottomPadding()))
}
}
}
} else {
LazyColumn(
state = listState,
modifier = Modifier
.then(
if (currentTheme.enableBlur) {
Modifier.hazeSource(state = hazeState)
} else {
Modifier
}
)
.fillMaxSize()
) {
item {
Spacer(modifier = Modifier.height(padding.calculateTopPadding()))
}
items(attachments) { item ->
ChatMaterialItem(
item = item,
onClick = {}
1 -> {
val viewModel: ChatMaterialsViewModel =
koinViewModel<ChatMaterialsViewModelImpl>(named(MaterialType.VIDEO))
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
val baseError by viewModel.baseError.collectAsStateWithLifecycle()
val canPaginate by viewModel.canPaginate.collectAsStateWithLifecycle()
VideoMaterialsScreen(
modifier = Modifier,
screenState = screenState,
baseError = baseError,
padding = padding,
onRefresh = viewModel::onRefresh,
onSessionExpiredLogOutButtonClicked = { },
setCanScrollBackward = { canScrollBackward = it },
canPaginate = canPaginate,
onPaginationConditionsMet = viewModel::onPaginationConditionsMet
)
}
item {
Spacer(modifier = Modifier.height(padding.calculateBottomPadding()))
2 -> {
val viewModel: ChatMaterialsViewModel =
koinViewModel<ChatMaterialsViewModelImpl>(named(MaterialType.AUDIO))
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
val baseError by viewModel.baseError.collectAsStateWithLifecycle()
val canPaginate by viewModel.canPaginate.collectAsStateWithLifecycle()
AudioMaterialsScreen(
modifier = Modifier,
screenState = screenState,
baseError = baseError,
padding = padding,
onRefresh = viewModel::onRefresh,
onSessionExpiredLogOutButtonClicked = { },
setCanScrollBackward = { canScrollBackward = it },
canPaginate = canPaginate,
onPaginationConditionsMet = viewModel::onPaginationConditionsMet
)
}
3 -> {
val viewModel: ChatMaterialsViewModel =
koinViewModel<ChatMaterialsViewModelImpl>(named(MaterialType.FILE))
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
val baseError by viewModel.baseError.collectAsStateWithLifecycle()
val canPaginate by viewModel.canPaginate.collectAsStateWithLifecycle()
FileMaterialsScreen(
modifier = Modifier,
screenState = screenState,
baseError = baseError,
padding = padding,
onRefresh = viewModel::onRefresh,
onSessionExpiredLogOutButtonClicked = { },
setCanScrollBackward = { canScrollBackward = it },
canPaginate = canPaginate,
onPaginationConditionsMet = viewModel::onPaginationConditionsMet
)
}
4 -> {
val viewModel: ChatMaterialsViewModel =
koinViewModel<ChatMaterialsViewModelImpl>(named(MaterialType.LINK))
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
val baseError by viewModel.baseError.collectAsStateWithLifecycle()
val canPaginate by viewModel.canPaginate.collectAsStateWithLifecycle()
LinkMaterialsScreen(
modifier = Modifier,
screenState = screenState,
baseError = baseError,
padding = padding,
onRefresh = viewModel::onRefresh,
onSessionExpiredLogOutButtonClicked = { },
setCanScrollBackward = { canScrollBackward = it },
canPaginate = canPaginate,
onPaginationConditionsMet = viewModel::onPaginationConditionsMet
)
}
else -> Unit
}
}
}
@@ -0,0 +1,248 @@
package dev.meloda.fast.chatmaterials.presentation.materials
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
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.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CircleShape
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.contentColorFor
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
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import dev.chrisbanes.haze.hazeSource
import dev.meloda.fast.chatmaterials.model.ChatMaterialsScreenState
import dev.meloda.fast.chatmaterials.model.UiChatMaterial
import dev.meloda.fast.model.BaseError
import dev.meloda.fast.ui.R
import dev.meloda.fast.ui.basic.ContentAlpha
import dev.meloda.fast.ui.basic.LocalContentAlpha
import dev.meloda.fast.ui.components.ErrorView
import dev.meloda.fast.ui.components.FullScreenLoader
import dev.meloda.fast.ui.components.NoItemsView
import dev.meloda.fast.ui.theme.LocalHazeState
import dev.meloda.fast.ui.theme.LocalThemeConfig
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import dev.meloda.fast.ui.R as UiR
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AudioMaterialsScreen(
modifier: Modifier = Modifier,
canPaginate: Boolean,
screenState: ChatMaterialsScreenState,
baseError: BaseError?,
padding: PaddingValues,
onRefresh: () -> Unit,
onSessionExpiredLogOutButtonClicked: () -> Unit,
setCanScrollBackward: (Boolean) -> Unit,
onPaginationConditionsMet: () -> Unit
) {
val coroutineScope = rememberCoroutineScope()
val hazeState = LocalHazeState.current
val currentTheme = LocalThemeConfig.current
val listState = rememberLazyListState()
val pullToRefreshState = rememberPullToRefreshState()
LaunchedEffect(listState) {
snapshotFlow { listState.canScrollBackward }
.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) {
is BaseError.SessionExpired -> {
ErrorView(
text = stringResource(UiR.string.session_expired),
buttonText = stringResource(UiR.string.action_log_out),
onButtonClick = onSessionExpiredLogOutButtonClicked
)
}
is BaseError.SimpleError -> {
ErrorView(
text = baseError.message,
buttonText = stringResource(UiR.string.try_again),
onButtonClick = onRefresh
)
}
}
}
screenState.isLoading && screenState.materials.isEmpty() -> FullScreenLoader()
else -> {
PullToRefreshBox(
modifier = modifier
.fillMaxSize()
.padding(start = padding.calculateStartPadding(LayoutDirection.Ltr))
.padding(end = padding.calculateEndPadding(LayoutDirection.Ltr)),
state = pullToRefreshState,
isRefreshing = screenState.isLoading,
onRefresh = onRefresh,
indicator = {
PullToRefreshDefaults.Indicator(
state = pullToRefreshState,
isRefreshing = screenState.isLoading,
modifier = Modifier
.align(Alignment.TopCenter)
.padding(top = padding.calculateTopPadding()),
)
}
) {
LazyColumn(
state = listState,
modifier = Modifier
.then(
if (currentTheme.enableBlur) {
Modifier.hazeSource(state = hazeState)
} else {
Modifier
}
)
.fillMaxSize()
) {
item {
Spacer(modifier = Modifier.height(padding.calculateTopPadding()))
}
items(screenState.materials) { item ->
item as UiChatMaterial.Audio
Row(
modifier = Modifier
.fillMaxWidth()
.heightIn(min = 64.dp)
.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
modifier = Modifier
.clip(CircleShape)
.background(MaterialTheme.colorScheme.primary)
.size(42.dp)
.padding(4.dp),
painter = painterResource(UiR.drawable.round_play_arrow_24),
contentDescription = null,
tint = contentColorFor(MaterialTheme.colorScheme.primary)
)
Spacer(modifier = Modifier.width(16.dp))
Column(modifier = Modifier.weight(1f)) {
Text(
text = item.title,
style = MaterialTheme.typography.bodyLarge,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(2.dp))
LocalContentAlpha(alpha = ContentAlpha.medium) {
Text(
text = item.artist,
style = MaterialTheme.typography.bodyMedium
)
}
}
Text(text = item.duration)
}
}
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()))
}
}
if (screenState.materials.isEmpty()) {
NoItemsView(
buttonText = stringResource(R.string.action_refresh),
onButtonClick = onRefresh
)
}
}
}
}
}
@@ -0,0 +1,224 @@
package dev.meloda.fast.chatmaterials.presentation.materials
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
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.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
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.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil.compose.AsyncImagePainter
import coil.compose.rememberAsyncImagePainter
import coil.imageLoader
import dev.chrisbanes.haze.hazeSource
import dev.meloda.fast.chatmaterials.model.ChatMaterialsScreenState
import dev.meloda.fast.chatmaterials.model.UiChatMaterial
import dev.meloda.fast.model.BaseError
import dev.meloda.fast.ui.R
import dev.meloda.fast.ui.basic.ContentAlpha
import dev.meloda.fast.ui.basic.LocalContentAlpha
import dev.meloda.fast.ui.components.ErrorView
import dev.meloda.fast.ui.components.FullScreenLoader
import dev.meloda.fast.ui.components.NoItemsView
import dev.meloda.fast.ui.theme.LocalHazeState
import dev.meloda.fast.ui.theme.LocalThemeConfig
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun FileMaterialsScreen(
modifier: Modifier = Modifier,
canPaginate: Boolean,
screenState: ChatMaterialsScreenState,
baseError: BaseError?,
padding: PaddingValues,
onRefresh: () -> Unit,
onSessionExpiredLogOutButtonClicked: () -> Unit,
setCanScrollBackward: (Boolean) -> Unit,
onPaginationConditionsMet: () -> Unit
) {
val hazeState = LocalHazeState.current
val currentTheme = LocalThemeConfig.current
val listState = rememberLazyListState()
val pullToRefreshState = rememberPullToRefreshState()
LaunchedEffect(listState) {
snapshotFlow { listState.canScrollBackward }
.collect(setCanScrollBackward)
}
when {
baseError != null -> {
when (baseError) {
is BaseError.SessionExpired -> {
ErrorView(
text = stringResource(R.string.session_expired),
buttonText = stringResource(R.string.action_log_out),
onButtonClick = onSessionExpiredLogOutButtonClicked
)
}
is BaseError.SimpleError -> {
ErrorView(
text = baseError.message,
buttonText = stringResource(R.string.try_again),
onButtonClick = onRefresh
)
}
}
}
screenState.isLoading && screenState.materials.isEmpty() -> FullScreenLoader()
else -> {
PullToRefreshBox(
modifier = modifier
.fillMaxSize()
.padding(start = padding.calculateStartPadding(LayoutDirection.Ltr))
.padding(end = padding.calculateEndPadding(LayoutDirection.Ltr)),
state = pullToRefreshState,
isRefreshing = screenState.isLoading,
onRefresh = onRefresh,
indicator = {
PullToRefreshDefaults.Indicator(
state = pullToRefreshState,
isRefreshing = screenState.isLoading,
modifier = Modifier
.align(Alignment.TopCenter)
.padding(top = padding.calculateTopPadding()),
)
}
) {
LazyColumn(
state = listState,
modifier = Modifier
.then(
if (currentTheme.enableBlur) {
Modifier.hazeSource(state = hazeState)
} else {
Modifier
}
)
.fillMaxSize()
) {
item {
Spacer(modifier = Modifier.height(padding.calculateTopPadding()))
}
items(screenState.materials) { item ->
item as UiChatMaterial.File
Row(
modifier = Modifier
.fillMaxWidth()
.heightIn(min = 64.dp)
.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
var errorLoading by remember {
mutableStateOf(false)
}
if (item.previewUrl != null && !errorLoading) {
Image(
modifier = Modifier
.clip(RoundedCornerShape(4.dp))
.size(width = 64.dp, height = 48.dp),
painter = rememberAsyncImagePainter(
model = item.previewUrl,
imageLoader = LocalContext.current.imageLoader,
onState = {
errorLoading = it is AsyncImagePainter.State.Error
}
),
contentDescription = null,
contentScale = ContentScale.Crop
)
} else {
Text(
modifier = Modifier
.clip(RoundedCornerShape(4.dp))
.background(
MaterialTheme.colorScheme
.surfaceColorAtElevation(3.dp)
)
.size(width = 64.dp, height = 48.dp)
.padding(4.dp),
text = item.extension.uppercase(),
textAlign = TextAlign.Center,
lineHeight = 40.sp,
fontWeight = FontWeight.Medium
)
}
Spacer(modifier = Modifier.width(16.dp))
Column(modifier = Modifier.weight(1f)) {
Text(
text = item.title,
style = MaterialTheme.typography.bodyLarge,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(2.dp))
LocalContentAlpha(alpha = ContentAlpha.medium) {
Text(
text = item.size,
style = MaterialTheme.typography.bodyMedium
)
}
}
}
}
item {
Spacer(modifier = Modifier.height(padding.calculateBottomPadding()))
}
}
if (screenState.materials.isEmpty()) {
NoItemsView(
buttonText = stringResource(R.string.action_refresh),
onButtonClick = onRefresh
)
}
}
}
}
}
@@ -0,0 +1,243 @@
package dev.meloda.fast.chatmaterials.presentation.materials
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
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.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
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.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil.compose.AsyncImagePainter
import coil.compose.rememberAsyncImagePainter
import coil.imageLoader
import dev.chrisbanes.haze.hazeSource
import dev.meloda.fast.chatmaterials.model.ChatMaterialsScreenState
import dev.meloda.fast.chatmaterials.model.UiChatMaterial
import dev.meloda.fast.model.BaseError
import dev.meloda.fast.ui.R
import dev.meloda.fast.ui.basic.ContentAlpha
import dev.meloda.fast.ui.basic.LocalContentAlpha
import dev.meloda.fast.ui.components.ErrorView
import dev.meloda.fast.ui.components.FullScreenLoader
import dev.meloda.fast.ui.components.NoItemsView
import dev.meloda.fast.ui.theme.LocalHazeState
import dev.meloda.fast.ui.theme.LocalThemeConfig
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun LinkMaterialsScreen(
modifier: Modifier = Modifier,
canPaginate: Boolean,
screenState: ChatMaterialsScreenState,
baseError: BaseError?,
padding: PaddingValues,
onRefresh: () -> Unit,
onSessionExpiredLogOutButtonClicked: () -> Unit,
setCanScrollBackward: (Boolean) -> Unit,
onPaginationConditionsMet: () -> Unit
) {
val hazeState = LocalHazeState.current
val currentTheme = LocalThemeConfig.current
val listState = rememberLazyListState()
val pullToRefreshState = rememberPullToRefreshState()
LaunchedEffect(listState) {
snapshotFlow { listState.canScrollBackward }
.collect(setCanScrollBackward)
}
when {
baseError != null -> {
when (baseError) {
is BaseError.SessionExpired -> {
ErrorView(
text = stringResource(R.string.session_expired),
buttonText = stringResource(R.string.action_log_out),
onButtonClick = onSessionExpiredLogOutButtonClicked
)
}
is BaseError.SimpleError -> {
ErrorView(
text = baseError.message,
buttonText = stringResource(R.string.try_again),
onButtonClick = onRefresh
)
}
}
}
screenState.isLoading && screenState.materials.isEmpty() -> FullScreenLoader()
else -> {
PullToRefreshBox(
modifier = modifier
.fillMaxSize()
.padding(start = padding.calculateStartPadding(LayoutDirection.Ltr))
.padding(end = padding.calculateEndPadding(LayoutDirection.Ltr)),
state = pullToRefreshState,
isRefreshing = screenState.isLoading,
onRefresh = onRefresh,
indicator = {
PullToRefreshDefaults.Indicator(
state = pullToRefreshState,
isRefreshing = screenState.isLoading,
modifier = Modifier
.align(Alignment.TopCenter)
.padding(top = padding.calculateTopPadding()),
)
}
) {
LazyColumn(
state = listState,
modifier = Modifier
.then(
if (currentTheme.enableBlur) {
Modifier.hazeSource(state = hazeState)
} else {
Modifier
}
)
.fillMaxSize()
) {
item {
Spacer(modifier = Modifier.height(padding.calculateTopPadding()))
}
items(screenState.materials) { item ->
item as UiChatMaterial.Link
Row(
modifier = Modifier
.fillMaxWidth()
.heightIn(min = 72.dp)
.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
var errorLoading by remember {
mutableStateOf(false)
}
if (item.previewUrl != null && !errorLoading) {
Image(
modifier = Modifier
.clip(RoundedCornerShape(4.dp))
.size(
width = 86.dp,
height = 64.dp
),
painter = rememberAsyncImagePainter(
model = item.previewUrl,
imageLoader = LocalContext.current.imageLoader,
onState = {
errorLoading = it is AsyncImagePainter.State.Error
}
),
contentDescription = null,
contentScale = ContentScale.Crop
)
} else {
Text(
modifier = Modifier
.clip(RoundedCornerShape(4.dp))
.background(
MaterialTheme.colorScheme
.surfaceColorAtElevation(3.dp)
)
.size(
width = 86.dp,
height = 64.dp
)
.padding(4.dp),
text = item.urlFirstChar,
textAlign = TextAlign.Center,
lineHeight = 56.sp,
fontWeight = FontWeight.Medium,
style = MaterialTheme.typography.titleLarge
)
}
Spacer(modifier = Modifier.width(16.dp))
Column(modifier = Modifier.weight(1f)) {
if (item.title != null) {
Text(
text = item.title,
style = MaterialTheme.typography.bodyLarge,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(2.dp))
}
LocalContentAlpha(
alpha = if (item.title != null) ContentAlpha.medium
else ContentAlpha.high
) {
Text(
text = item.url,
style = if (item.title != null) {
MaterialTheme.typography.bodyMedium
} else {
MaterialTheme.typography.bodyLarge
},
maxLines = if (item.title != null) 1 else 2,
overflow = TextOverflow.Ellipsis
)
}
}
}
}
item {
Spacer(modifier = Modifier.height(padding.calculateBottomPadding()))
}
}
if (screenState.materials.isEmpty()) {
NoItemsView(
buttonText = stringResource(R.string.action_refresh),
onButtonClick = onRefresh
)
}
}
}
}
}
@@ -0,0 +1,161 @@
package dev.meloda.fast.chatmaterials.presentation.materials
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
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.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.material3.ExperimentalMaterial3Api
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.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import dev.chrisbanes.haze.hazeSource
import dev.meloda.fast.chatmaterials.model.ChatMaterialsScreenState
import dev.meloda.fast.chatmaterials.model.UiChatMaterial
import dev.meloda.fast.model.BaseError
import dev.meloda.fast.ui.R
import dev.meloda.fast.ui.components.ErrorView
import dev.meloda.fast.ui.components.FullScreenLoader
import dev.meloda.fast.ui.components.NoItemsView
import dev.meloda.fast.ui.theme.LocalHazeState
import dev.meloda.fast.ui.theme.LocalThemeConfig
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PhotoMaterialsScreen(
modifier: Modifier = Modifier,
canPaginate: Boolean,
screenState: ChatMaterialsScreenState,
baseError: BaseError?,
padding: PaddingValues,
onRefresh: () -> Unit,
onSessionExpiredLogOutButtonClicked: () -> Unit,
setCanScrollBackward: (Boolean) -> Unit,
onPhotoClicked: (String) -> Unit,
onPaginationConditionsMet: () -> Unit
) {
val hazeState = LocalHazeState.current
val currentTheme = LocalThemeConfig.current
val gridState = rememberLazyGridState()
val pullToRefreshState = rememberPullToRefreshState()
LaunchedEffect(gridState) {
snapshotFlow { gridState.canScrollBackward }
.collect(setCanScrollBackward)
}
when {
baseError != null -> {
when (baseError) {
is BaseError.SessionExpired -> {
ErrorView(
text = stringResource(R.string.session_expired),
buttonText = stringResource(R.string.action_log_out),
onButtonClick = onSessionExpiredLogOutButtonClicked
)
}
is BaseError.SimpleError -> {
ErrorView(
text = baseError.message,
buttonText = stringResource(R.string.try_again),
onButtonClick = onRefresh
)
}
}
}
screenState.isLoading && screenState.materials.isEmpty() -> FullScreenLoader()
else -> {
PullToRefreshBox(
modifier = modifier
.fillMaxSize()
.padding(start = padding.calculateStartPadding(LayoutDirection.Ltr))
.padding(end = padding.calculateEndPadding(LayoutDirection.Ltr)),
state = pullToRefreshState,
isRefreshing = screenState.isLoading,
onRefresh = onRefresh,
indicator = {
PullToRefreshDefaults.Indicator(
state = pullToRefreshState,
isRefreshing = screenState.isLoading,
modifier = Modifier
.align(Alignment.TopCenter)
.padding(top = padding.calculateTopPadding()),
)
}
) {
LazyVerticalGrid(
columns = GridCells.Fixed(3),
state = gridState,
horizontalArrangement = Arrangement.spacedBy(2.dp),
modifier = Modifier
.then(
if (currentTheme.enableBlur) {
Modifier.hazeSource(state = hazeState)
} else {
Modifier
}
)
.fillMaxSize()
) {
repeat(3) {
item {
Spacer(modifier = Modifier.height(padding.calculateTopPadding()))
}
}
items(items = screenState.materials) { item ->
item as UiChatMaterial.Photo
AsyncImage(
model = item.previewUrl,
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.fillMaxWidth()
.aspectRatio(1f)
.clickable(
onClick = {
onPhotoClicked(item.previewUrl)
}
)
)
}
repeat(3) {
item {
Spacer(modifier = Modifier.height(padding.calculateBottomPadding()))
}
}
}
if (screenState.materials.isEmpty()) {
NoItemsView(
buttonText = stringResource(R.string.action_refresh),
onButtonClick = onRefresh
)
}
}
}
}
}
@@ -0,0 +1,214 @@
package dev.meloda.fast.chatmaterials.presentation.materials
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
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.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
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.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
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.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import coil.compose.rememberAsyncImagePainter
import coil.imageLoader
import dev.chrisbanes.haze.hazeSource
import dev.meloda.fast.chatmaterials.model.ChatMaterialsScreenState
import dev.meloda.fast.chatmaterials.model.UiChatMaterial
import dev.meloda.fast.model.BaseError
import dev.meloda.fast.ui.R
import dev.meloda.fast.ui.basic.ContentAlpha
import dev.meloda.fast.ui.basic.LocalContentAlpha
import dev.meloda.fast.ui.components.ErrorView
import dev.meloda.fast.ui.components.FullScreenLoader
import dev.meloda.fast.ui.components.NoItemsView
import dev.meloda.fast.ui.theme.LocalHazeState
import dev.meloda.fast.ui.theme.LocalThemeConfig
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun VideoMaterialsScreen(
modifier: Modifier = Modifier,
canPaginate: Boolean,
screenState: ChatMaterialsScreenState,
baseError: BaseError?,
padding: PaddingValues,
onRefresh: () -> Unit,
onSessionExpiredLogOutButtonClicked: () -> Unit,
setCanScrollBackward: (Boolean) -> Unit,
onPaginationConditionsMet: () -> Unit
) {
val hazeState = LocalHazeState.current
val currentTheme = LocalThemeConfig.current
val listState = rememberLazyListState()
val pullToRefreshState = rememberPullToRefreshState()
LaunchedEffect(listState) {
snapshotFlow { listState.canScrollBackward }
.collect(setCanScrollBackward)
}
when {
baseError != null -> {
when (baseError) {
is BaseError.SessionExpired -> {
ErrorView(
text = stringResource(R.string.session_expired),
buttonText = stringResource(R.string.action_log_out),
onButtonClick = onSessionExpiredLogOutButtonClicked
)
}
is BaseError.SimpleError -> {
ErrorView(
text = baseError.message,
buttonText = stringResource(R.string.try_again),
onButtonClick = onRefresh
)
}
}
}
screenState.isLoading && screenState.materials.isEmpty() -> FullScreenLoader()
else -> {
PullToRefreshBox(
modifier = modifier
.fillMaxSize()
.padding(start = padding.calculateStartPadding(LayoutDirection.Ltr))
.padding(end = padding.calculateEndPadding(LayoutDirection.Ltr)),
state = pullToRefreshState,
isRefreshing = screenState.isLoading,
onRefresh = onRefresh,
indicator = {
PullToRefreshDefaults.Indicator(
state = pullToRefreshState,
isRefreshing = screenState.isLoading,
modifier = Modifier
.align(Alignment.TopCenter)
.padding(top = padding.calculateTopPadding()),
)
}
) {
LazyColumn(
state = listState,
modifier = Modifier
.then(
if (currentTheme.enableBlur) {
Modifier.hazeSource(state = hazeState)
} else {
Modifier
}
)
.fillMaxSize()
) {
item {
Spacer(modifier = Modifier.height(padding.calculateTopPadding()))
}
items(screenState.materials) { item ->
item as UiChatMaterial.Video
Row(
modifier = Modifier
.fillMaxWidth()
.heightIn(min = 72.dp)
.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Box {
Image(
modifier = Modifier
.fillMaxWidth(0.33f)
.height(64.dp)
.clip(RoundedCornerShape(4.dp)),
painter = rememberAsyncImagePainter(
model = item.previewUrl,
imageLoader = LocalContext.current.imageLoader
),
contentDescription = null,
contentScale = ContentScale.Crop
)
Text(
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(
end = 4.dp,
bottom = 4.dp
)
.clip(RoundedCornerShape(8.dp))
.background(
MaterialTheme.colorScheme.onBackground.copy(alpha = 0.6f)
)
.padding(
vertical = 1.dp,
horizontal = 4.dp
),
text = item.duration,
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.background
)
}
Spacer(modifier = Modifier.width(16.dp))
Column(modifier = Modifier.weight(1f)) {
Text(
text = item.title,
style = MaterialTheme.typography.bodyLarge,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(2.dp))
LocalContentAlpha(alpha = ContentAlpha.medium) {
Text(
text = "${item.views} views",
style = MaterialTheme.typography.bodyMedium
)
}
}
}
}
item {
Spacer(modifier = Modifier.height(padding.calculateBottomPadding()))
}
}
if (screenState.materials.isEmpty()) {
NoItemsView(
buttonText = stringResource(R.string.action_refresh),
onButtonClick = onRefresh
)
}
}
}
}
}
@@ -1,6 +1,7 @@
package dev.meloda.fast.chatmaterials.util
import dev.meloda.fast.chatmaterials.model.UiChatMaterial
import dev.meloda.fast.common.util.AndroidUtils
import dev.meloda.fast.model.api.data.AttachmentType
import dev.meloda.fast.model.api.domain.VkAttachmentHistoryMessage
import dev.meloda.fast.model.api.domain.VkAudioDomain
@@ -8,7 +9,6 @@ import dev.meloda.fast.model.api.domain.VkFileDomain
import dev.meloda.fast.model.api.domain.VkLinkDomain
import dev.meloda.fast.model.api.domain.VkPhotoDomain
import dev.meloda.fast.model.api.domain.VkVideoDomain
import java.text.SimpleDateFormat
import java.util.Locale
fun VkAttachmentHistoryMessage.asPresentation(): UiChatMaterial =
@@ -22,36 +22,110 @@ fun VkAttachmentHistoryMessage.asPresentation(): UiChatMaterial =
AttachmentType.VIDEO -> {
val attachment = this.attachment as VkVideoDomain
val duration = attachment.duration
val days = duration / (24 * 3600)
val hours = (duration % (24 * 3600)) / 3600
val minutes = (duration % 3600) / 60
val seconds = duration % 60
val args = mutableListOf<Int>()
if (days > 0) args.add(days)
if (hours > 0) args.add(hours)
args.add(minutes)
args.add(seconds)
val builder = StringBuilder()
if (days > 0) builder.append("%02d:")
if (hours > 0) builder.append("%02d:")
builder.append("%02d:%02d")
val formattedDuration =
builder.toString().format(Locale.getDefault(), *args.toTypedArray())
UiChatMaterial.Video(
previewUrl = attachment.images.firstOrNull()?.url.orEmpty()
previewUrl = attachment.images.maxByOrNull(VkVideoDomain.VideoImage::width)?.url.orEmpty(),
title = attachment.title,
views = attachment.views,
duration = formattedDuration
)
}
AttachmentType.AUDIO -> {
val attachment = this.attachment as VkAudioDomain
val duration = attachment.duration
val days = duration / (24 * 3600)
val hours = (duration % (24 * 3600)) / 3600
val minutes = (duration % 3600) / 60
val seconds = duration % 60
val args = mutableListOf<Int>()
if (days > 0) args.add(days)
if (hours > 0) args.add(hours)
args.add(minutes)
args.add(seconds)
val builder = StringBuilder()
if (days > 0) builder.append("%02d:")
if (hours > 0) builder.append("%02d:")
builder.append("%d:%02d")
val formattedDuration =
builder.toString().format(Locale.getDefault(), *args.toTypedArray())
UiChatMaterial.Audio(
previewUrl = null,
title = attachment.title,
artist = attachment.artist,
duration = SimpleDateFormat(
"mm:ss",
Locale.getDefault()
).format(attachment.duration)
duration = formattedDuration
)
}
AttachmentType.FILE -> {
val attachment = this.attachment as VkFileDomain
val previewUrl: String? = when (val preview = attachment.preview) {
null -> null
else -> {
when {
preview.photo != null -> {
val size = preview.photo?.sizes?.maxByOrNull { it.width }
size?.src
}
preview.video != null -> {
val size = preview.video?.src
size
}
else -> null
}
}
}
UiChatMaterial.File(
title = attachment.title
title = attachment.title,
previewUrl = previewUrl,
size = AndroidUtils.bytesToHumanReadableSize(attachment.size.toDouble()),
extension = attachment.ext.take(4)
)
}
AttachmentType.LINK -> {
val attachment = this.attachment as VkLinkDomain
UiChatMaterial.Link(
title = attachment.title ?: attachment.url,
previewUrl = attachment.photo?.getMaxSize()?.url
title = attachment.title,
previewUrl = attachment.photo?.getMaxSize()?.url,
url = attachment.url,
urlFirstChar = attachment.url.replaceFirst("http://", "")
.replaceFirst("https://", "")
.take(1)
.uppercase()
)
}
@@ -13,7 +13,7 @@ data class ConversationsScreenState(
val isPaginationExhausted: Boolean,
val profileImageUrl: String?,
val scrollIndex: Int,
val scrollOffset: Int
val scrollOffset: Int,
) {
companion object {
@@ -137,7 +137,6 @@ fun ConversationsScreen(
setScrollIndex: (Int) -> Unit = {},
setScrollOffset: (Int) -> Unit = {}
) {
val view = LocalView.current
val currentTheme = LocalThemeConfig.current
val maxLines by remember(currentTheme) {
@@ -1,6 +1,7 @@
package dev.meloda.fast.conversations.presentation
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.FastOutLinearInEasing
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
@@ -79,8 +80,6 @@ fun CreateChatRoute(
onChatCreated: (Int) -> Unit,
viewModel: CreateChatViewModel
) {
val context = LocalContext.current
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
val baseError by viewModel.baseError.collectAsStateWithLifecycle()
val canPaginate by viewModel.canPaginate.collectAsStateWithLifecycle()
@@ -148,20 +147,24 @@ fun CreateChatScreen(
val hazeState = LocalHazeState.current
val toolbarColorAlpha by animateFloatAsState(
targetValue = if (!listState.canScrollBackward) 1f else 0f,
val topBarContainerColorAlpha by animateFloatAsState(
targetValue = if (!currentTheme.enableBlur || !listState.canScrollBackward) 1f else 0f,
label = "toolbarColorAlpha",
animationSpec = tween(durationMillis = 50)
animationSpec = tween(
durationMillis = 200,
easing = FastOutLinearInEasing
)
)
val toolbarContainerColor by animateColorAsState(
val topBarContainerColor by animateColorAsState(
targetValue =
if (currentTheme.enableBlur || !listState.canScrollBackward)
MaterialTheme.colorScheme.surface
else
MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp),
if (currentTheme.enableBlur || !listState.canScrollBackward) MaterialTheme.colorScheme.surface
else MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp),
label = "toolbarColorAlpha",
animationSpec = tween(durationMillis = 50)
animationSpec = tween(
durationMillis = 200,
easing = FastOutLinearInEasing
)
)
Scaffold(
@@ -171,11 +174,7 @@ fun CreateChatScreen(
Column(
modifier = Modifier
.fillMaxWidth()
.background(
toolbarContainerColor.copy(
alpha = if (currentTheme.enableBlur) toolbarColorAlpha else 1f
)
)
.background(topBarContainerColor.copy(alpha = topBarContainerColorAlpha))
.then(
if (currentTheme.enableBlur) {
Modifier.hazeEffect(
@@ -205,11 +204,7 @@ fun CreateChatScreen(
style = MaterialTheme.typography.headlineSmall
)
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = toolbarContainerColor.copy(
alpha = 0f
)
),
colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent),
modifier = Modifier.fillMaxWidth(),
)
@@ -24,14 +24,13 @@ 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.derivedStateOf
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.rememberCoroutineScope
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.painterResource
@@ -50,7 +49,7 @@ import dev.meloda.fast.ui.model.TabItem
import dev.meloda.fast.ui.theme.LocalHazeState
import dev.meloda.fast.ui.theme.LocalThemeConfig
import dev.meloda.fast.ui.util.ImmutableList
import kotlinx.coroutines.launch
import dev.meloda.fast.ui.R as UiR
@OptIn(ExperimentalMaterial3Api::class, ExperimentalHazeMaterialsApi::class)
@@ -60,10 +59,7 @@ fun FriendsRoute(
onPhotoClicked: (url: String) -> Unit,
onMessageClicked: (userId: Int) -> Unit,
) {
var selectedTabIndex by rememberSaveable {
mutableIntStateOf(0)
}
val scope = rememberCoroutineScope()
val currentTheme = LocalThemeConfig.current
val hazeState = LocalHazeState.current
@@ -107,20 +103,36 @@ fun FriendsRoute(
)
}
val pagerState = rememberPagerState(pageCount = tabItems::size)
val selectedTabIndex by remember {
derivedStateOf { pagerState.currentPage }
}
var orderType: String by remember { mutableStateOf("hints") }
var showOrderDialog by remember { mutableStateOf(false) }
val orderItems = remember {
mapOf(
"hints" to "Priority",
"name" to "Name",
"random" to "Random",
"mobile" to "Mobile",
"smart" to "Smart"
val orderPriority = stringResource(UiR.string.friends_order_priority)
val orderName = stringResource(UiR.string.friends_order_name)
val orderRandom = stringResource(UiR.string.friends_order_random)
val orderMobile = stringResource(UiR.string.friends_order_mobile)
val orderSmart = stringResource(UiR.string.friends_order_smart)
val orderTitleItems = remember {
ImmutableList.of(
orderPriority,
orderName,
orderRandom,
orderMobile,
orderSmart
)
}
val orderItems = remember {
listOf("hints", "name", "random", "mobile", "smart")
}
var selectedIndex by remember {
mutableIntStateOf(0)
}
@@ -130,17 +142,16 @@ fun FriendsRoute(
onDismissRequest = { showOrderDialog = false },
confirmText = stringResource(R.string.ok),
confirmAction = {
orderType =
orderItems.keys.toCollection(mutableListOf())[selectedIndex]
orderType = orderItems[selectedIndex]
},
cancelText = stringResource(R.string.cancel),
selectionType = SelectionType.Single,
items = ImmutableList.copyOf(orderItems.values),
items = orderTitleItems,
preSelectedItems = ImmutableList.of(selectedIndex),
onItemClick = {
selectedIndex = it
},
title = "Order type",
title = stringResource(UiR.string.friends_order_by_title),
actionInvokeDismiss = ActionInvokeDismiss.Always
)
}
@@ -199,8 +210,8 @@ fun FriendsRoute(
Tab(
selected = index == selectedTabIndex,
onClick = {
if (selectedTabIndex != index) {
selectedTabIndex = index
scope.launch {
pagerState.animateScrollToPage(index)
}
},
text = {
@@ -214,21 +225,6 @@ fun FriendsRoute(
}
}
) { 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(),
@@ -229,8 +229,7 @@ fun MessagesHistoryScreen(
.fillMaxWidth(),
title = {
Row(
modifier = Modifier
.weight(1f),
modifier = Modifier.weight(1f),
verticalAlignment = Alignment.CenterVertically
) {
if (selectedMessages.isEmpty()) {
@@ -332,6 +331,9 @@ fun MessagesHistoryScreen(
)
}
} else {
if (screenState.isLoading) {
return@TopAppBar
}
IconButton(
onClick = { dropDownMenuExpanded = true }
) {
@@ -362,7 +364,13 @@ fun MessagesHistoryScreen(
)
},
text = {
Text(text = "Materials")
Text(text = stringResource(UiR.string.chat_materials_action_title))
},
leadingIcon = {
Icon(
painter = painterResource(UiR.drawable.ic_multimedia),
contentDescription = null
)
}
)
DropdownMenuItem(
@@ -371,7 +379,7 @@ fun MessagesHistoryScreen(
dropDownMenuExpanded = false
},
text = {
Text(text = "Refresh")
Text(text = stringResource(UiR.string.action_refresh))
},
leadingIcon = {
Icon(