[wip] chat materials; some experiments with local composition and blur

This commit is contained in:
2024-07-12 00:51:24 +03:00
parent c43278e4cf
commit fb76b46b22
46 changed files with 1210 additions and 717 deletions
+1
View File
@@ -27,6 +27,7 @@ android {
}
kotlinOptions {
jvmTarget = Configs.java.toString()
freeCompilerArgs = listOf("-opt-in=kotlin.RequiresOptIn", "-Xcontext-receivers")
}
buildFeatures {
compose = true
@@ -1,91 +0,0 @@
package com.meloda.app.fast.chatmaterials
import android.annotation.SuppressLint
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import dev.chrisbanes.haze.HazeState
import dev.chrisbanes.haze.haze
import dev.chrisbanes.haze.hazeChild
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
import dev.chrisbanes.haze.materials.HazeMaterials
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@OptIn(
ExperimentalMaterial3Api::class,
ExperimentalHazeMaterialsApi::class
)
@Composable
fun ChatMaterialsScreen(
onBack: () -> Unit
) {
val hazeState = remember { HazeState() }
Scaffold(
topBar = {
TopAppBar(
title = {
Text(text = "Chat Materials")
},
colors = TopAppBarDefaults.largeTopAppBarColors(Color.Transparent),
modifier = Modifier
.hazeChild(
state = hazeState,
style = HazeMaterials.ultraThin()
)
.fillMaxWidth(),
navigationIcon = {
IconButton(
onClick = onBack
) {
Icon(
imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
contentDescription = null
)
}
}
)
}
) {
LazyVerticalGrid(
columns = GridCells.Adaptive(200.dp),
horizontalArrangement = Arrangement.spacedBy(4.dp),
modifier = Modifier
.haze(
state = hazeState,
style = HazeMaterials.ultraThin()
)
) {
items(100) { index ->
val link = "https://random.imagecdn.app/500/150"
AsyncImage(
model = link,
contentDescription = "Image",
contentScale = ContentScale.Crop,
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
)
}
}
}
}
@@ -0,0 +1,130 @@
package com.meloda.app.fast.chatmaterials
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import com.meloda.app.fast.chatmaterials.model.ChatMaterialsScreenState
import com.meloda.app.fast.chatmaterials.navigation.ChatMaterials
import com.meloda.app.fast.chatmaterials.util.asPresentation
import com.meloda.app.fast.common.extensions.listenValue
import com.meloda.app.fast.common.extensions.setValue
import com.meloda.app.fast.data.api.messages.MessagesUseCase
import com.meloda.app.fast.data.processState
import com.meloda.app.fast.model.BaseError
import com.meloda.app.fast.model.api.domain.VkAttachmentHistoryMessage
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
interface ChatMaterialsViewModel {
val screenState: StateFlow<ChatMaterialsScreenState>
val baseError: StateFlow<BaseError?>
val imagesToPreload: StateFlow<List<String>>
val currentOffset: StateFlow<Int>
val canPaginate: StateFlow<Boolean>
fun onMetPaginationCondition()
fun onRefresh()
fun onErrorConsumed()
fun onTypeChanged(newType: String)
}
class ChatMaterialsViewModelImpl(
private val messagesUseCase: MessagesUseCase,
savedStateHandle: SavedStateHandle
) : ViewModel(), ChatMaterialsViewModel {
override val screenState = MutableStateFlow(ChatMaterialsScreenState.EMPTY)
override val baseError = MutableStateFlow<BaseError?>(null)
override val imagesToPreload = MutableStateFlow<List<String>>(emptyList())
override val currentOffset = MutableStateFlow(0)
override val canPaginate = MutableStateFlow(false)
init {
val arguments = ChatMaterials.from(savedStateHandle)
screenState.setValue { old ->
old.copy(
peerId = arguments.peerId,
conversationMessageId = arguments.conversationMessageId
)
}
loadChatMaterials()
}
override fun onMetPaginationCondition() {
currentOffset.update { screenState.value.materials.size }
loadChatMaterials()
}
override fun onRefresh() {
loadChatMaterials(offset = 0)
}
override fun onErrorConsumed() {
baseError.setValue { null }
}
override fun onTypeChanged(newType: String) {
screenState.setValue { old -> old.copy(attachmentType = newType) }
loadChatMaterials(0)
}
private fun loadChatMaterials(
offset: Int = currentOffset.value
) {
messagesUseCase.getHistoryAttachments(
peerId = screenState.value.peerId,
count = LOAD_COUNT,
offset = offset,
attachmentTypes = listOf(screenState.value.attachmentType),
conversationMessageId = screenState.value.conversationMessageId
).listenValue { state ->
state.processState(
error = { error ->
},
success = { response ->
val itemsCountSufficient = response.size == LOAD_COUNT
canPaginate.setValue { itemsCountSufficient }
val paginationExhausted = !itemsCountSufficient &&
screenState.value.materials.size >= LOAD_COUNT
val loadedMaterials = response.map(VkAttachmentHistoryMessage::asPresentation)
val newState = screenState.value.copy(
isPaginationExhausted = paginationExhausted
)
if (offset == 0) {
screenState.setValue {
newState.copy(materials = loadedMaterials)
}
} else {
screenState.setValue {
newState.copy(
materials = newState.materials.plus(loadedMaterials)
)
}
}
}
)
screenState.setValue { old ->
old.copy(
isLoading = offset == 0 && state.isLoading(),
isPaginating = offset > 0 && state.isLoading()
)
}
}
}
companion object {
const val LOAD_COUNT = 100
}
}
@@ -0,0 +1,9 @@
package com.meloda.app.fast.chatmaterials.di
import com.meloda.app.fast.chatmaterials.ChatMaterialsViewModelImpl
import org.koin.androidx.viewmodel.dsl.viewModelOf
import org.koin.dsl.module
val chatMaterialsModule = module {
viewModelOf(::ChatMaterialsViewModelImpl)
}
@@ -0,0 +1,24 @@
package com.meloda.app.fast.chatmaterials.model
data class ChatMaterialsScreenState(
val isLoading: Boolean,
val materials: List<UiChatMaterial>,
val attachmentType: String,
val isPaginating: Boolean,
val isPaginationExhausted: Boolean,
val peerId: Int,
val conversationMessageId: Int
) {
companion object {
val EMPTY: ChatMaterialsScreenState = ChatMaterialsScreenState(
isLoading = true,
materials = emptyList(),
attachmentType = "photo",
isPaginating = false,
isPaginationExhausted = false,
peerId = -1,
conversationMessageId = -1
)
}
}
@@ -0,0 +1,28 @@
package com.meloda.app.fast.chatmaterials.model
sealed class UiChatMaterial {
data class Photo(
val previewUrl: String
) : UiChatMaterial()
data class Video(
val previewUrl: String
) : UiChatMaterial()
data class Audio(
val previewUrl: String?,
val title: String,
val artist: String,
val duration: String
) : UiChatMaterial()
data class File(
val title: String
) : UiChatMaterial()
data class Link(
val title: String,
val previewUrl: String?
) : UiChatMaterial()
}
@@ -1,13 +1,23 @@
package com.meloda.app.fast.chatmaterials.navigation
import androidx.lifecycle.SavedStateHandle
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable
import com.meloda.app.fast.chatmaterials.ChatMaterialsScreen
import androidx.navigation.toRoute
import com.meloda.app.fast.chatmaterials.presentation.ChatMaterialsScreen
import kotlinx.serialization.Serializable
@Serializable
data class ChatMaterials(val a: String)
data class ChatMaterials(
val peerId: Int,
val conversationMessageId: Int
) {
companion object {
fun from(savedStateHandle: SavedStateHandle) =
savedStateHandle.toRoute<ChatMaterials>()
}
}
fun NavGraphBuilder.chatMaterialsRoute(
onBack: () -> Unit
@@ -19,6 +29,11 @@ fun NavGraphBuilder.chatMaterialsRoute(
}
}
fun NavController.navigateToChatMaterials() {
this.navigate(ChatMaterials(""))
fun NavController.navigateToChatMaterials(peerId: Int, conversationMessageId: Int) {
this.navigate(
ChatMaterials(
peerId = peerId,
conversationMessageId = conversationMessageId
)
)
}
@@ -0,0 +1,68 @@
package com.meloda.app.fast.chatmaterials.presentation
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import coil.ImageLoader
import coil.compose.AsyncImage
import com.meloda.app.fast.chatmaterials.model.UiChatMaterial
@Composable
fun ChatMaterialItem(
item: UiChatMaterial,
imageLoader: ImageLoader
) {
when (item) {
is UiChatMaterial.Photo -> {
AsyncImage(
model = item.previewUrl,
contentDescription = null,
imageLoader = imageLoader,
contentScale = ContentScale.Crop,
modifier = Modifier
.fillMaxWidth()
.aspectRatio(1f)
)
}
is UiChatMaterial.Video -> {
AsyncImage(
model = item.previewUrl,
contentDescription = null,
imageLoader = imageLoader,
contentScale = ContentScale.Crop,
modifier = Modifier
.fillMaxWidth()
.aspectRatio(1f)
)
}
is UiChatMaterial.Audio -> {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = item.title,
style = MaterialTheme.typography.bodyLarge
)
Text(text = item.artist)
}
Text(text = item.duration)
}
}
is UiChatMaterial.File -> {}
is UiChatMaterial.Link -> {}
}
}
@@ -0,0 +1,352 @@
package com.meloda.app.fast.chatmaterials.presentation
import android.annotation.SuppressLint
import android.util.Log
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
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.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.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.pulltorefresh.PullToRefreshContainer
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coil.imageLoader
import com.meloda.app.fast.chatmaterials.ChatMaterialsViewModel
import com.meloda.app.fast.chatmaterials.ChatMaterialsViewModelImpl
import com.meloda.app.fast.designsystem.LocalTheme
import com.meloda.app.fast.designsystem.R
import dev.chrisbanes.haze.HazeState
import dev.chrisbanes.haze.haze
import dev.chrisbanes.haze.hazeChild
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
import dev.chrisbanes.haze.materials.HazeMaterials
import org.koin.androidx.compose.koinViewModel
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@OptIn(
ExperimentalMaterial3Api::class,
ExperimentalHazeMaterialsApi::class
)
@Composable
fun ChatMaterialsScreen(
onBack: () -> Unit,
viewModel: ChatMaterialsViewModel = koinViewModel<ChatMaterialsViewModelImpl>()
) {
val currentTheme = LocalTheme.current
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
val attachments = screenState.materials
val imageLoader = LocalContext.current.imageLoader
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) {
viewModel.onTypeChanged(
when (checkedTypeIndex) {
0 -> "photo"
1 -> "video"
2 -> "audio"
3 -> "doc"
4 -> "link"
else -> ""
}
)
}
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
}
Log.d("ChatMaterialsScreen", "ChatMaterialsScreen: canScrollBackward: $canScrollBackward")
val toolbarColorAlpha by animateFloatAsState(
targetValue = if (!canScrollBackward) 1f else 0f,
label = "toolbarColorAlpha",
animationSpec = tween(durationMillis = 50)
)
val toolbarContainerColor by animateColorAsState(
targetValue =
if (currentTheme.usingBlur || !canScrollBackward)
MaterialTheme.colorScheme.surface
else
MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp),
label = "toolbarColorAlpha",
animationSpec = tween(durationMillis = 50)
)
val pullToRefreshAlpha by animateFloatAsState(
targetValue = if (!canScrollBackward) 1f else 0f,
label = "pullToRefreshAlpha",
animationSpec = tween(durationMillis = 50)
)
val pullToRefreshState = rememberPullToRefreshState()
Scaffold(
topBar = {
TopAppBar(
title = {
Text(text = "Chat Materials")
},
modifier = Modifier
.then(
if (currentTheme.usingBlur) {
Modifier.hazeChild(
state = hazeState,
style = hazeStyle
)
} else Modifier
)
.fillMaxWidth(),
colors = TopAppBarDefaults.topAppBarColors(
containerColor = toolbarContainerColor.copy(
alpha = if (currentTheme.usingBlur) toolbarColorAlpha else 1f
)
),
navigationIcon = {
IconButton(onClick = onBack) {
Icon(
imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
contentDescription = null
)
}
},
actions = {
IconButton(
onClick = {
dropDownMenuExpanded = true
}
) {
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(
onClick = {
viewModel.onRefresh()
dropDownMenuExpanded = false
},
text = {
Text(text = stringResource(id = R.string.action_refresh))
},
leadingIcon = {
Icon(
imageVector = Icons.Rounded.Refresh,
contentDescription = null
)
}
)
if (currentTheme.usingBlur) {
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
}
)
}
}
}
)
}
) { padding ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(start = padding.calculateStartPadding(LayoutDirection.Ltr))
.padding(end = padding.calculateEndPadding(LayoutDirection.Ltr))
.nestedScroll(pullToRefreshState.nestedScrollConnection)
) {
if (checkedTypeIndex in listOf(0, 1)) {
LazyVerticalGrid(
columns = GridCells.Fixed(3),
state = gridState,
horizontalArrangement = Arrangement.spacedBy(2.dp),
modifier = Modifier
.then(
if (currentTheme.usingBlur) {
Modifier.haze(
state = hazeState,
style = hazeStyle
)
} else {
Modifier
}
)
.fillMaxSize()
) {
repeat(3) {
item {
Spacer(modifier = Modifier.height(padding.calculateTopPadding()))
}
}
items(attachments) { item ->
ChatMaterialItem(
item = item,
imageLoader = imageLoader
)
}
repeat(3) {
item {
Spacer(modifier = Modifier.height(padding.calculateBottomPadding()))
}
}
}
} else {
LazyColumn(
state = listState,
modifier = Modifier
.then(
if (currentTheme.usingBlur) {
Modifier.haze(
state = hazeState,
style = hazeStyle
)
} else {
Modifier
}
)
.fillMaxSize()
) {
item {
Spacer(modifier = Modifier.height(padding.calculateTopPadding()))
}
items(attachments) { item ->
ChatMaterialItem(
item = item,
imageLoader = imageLoader
)
}
item {
Spacer(modifier = Modifier.height(padding.calculateBottomPadding()))
}
}
}
if (pullToRefreshState.isRefreshing) {
LaunchedEffect(true) {
viewModel.onRefresh()
}
}
LaunchedEffect(screenState.isLoading) {
if (!screenState.isLoading) {
pullToRefreshState.endRefresh()
}
}
PullToRefreshContainer(
state = pullToRefreshState,
modifier = Modifier
.alpha(pullToRefreshAlpha)
.align(Alignment.TopCenter)
.padding(top = padding.calculateTopPadding()),
contentColor = MaterialTheme.colorScheme.primary
)
}
}
}
@@ -0,0 +1,59 @@
package com.meloda.app.fast.chatmaterials.util
import com.meloda.app.fast.chatmaterials.model.UiChatMaterial
import com.meloda.app.fast.model.api.data.AttachmentType
import com.meloda.app.fast.model.api.domain.VkAttachmentHistoryMessage
import com.meloda.app.fast.model.api.domain.VkAudioDomain
import com.meloda.app.fast.model.api.domain.VkFileDomain
import com.meloda.app.fast.model.api.domain.VkLinkDomain
import com.meloda.app.fast.model.api.domain.VkPhotoDomain
import com.meloda.app.fast.model.api.domain.VkVideoDomain
import java.text.SimpleDateFormat
import java.util.Locale
fun VkAttachmentHistoryMessage.asPresentation(): UiChatMaterial =
when (val type = this.attachment.type) {
AttachmentType.PHOTO -> {
val attachment = this.attachment as VkPhotoDomain
UiChatMaterial.Photo(
previewUrl = attachment.getSizeOrSmaller(VkPhotoDomain.SIZE_TYPE_1080_1024)?.url.orEmpty()
)
}
AttachmentType.VIDEO -> {
val attachment = this.attachment as VkVideoDomain
UiChatMaterial.Video(
previewUrl = attachment.images.firstOrNull()?.url.orEmpty()
)
}
AttachmentType.AUDIO -> {
val attachment = this.attachment as VkAudioDomain
UiChatMaterial.Audio(
previewUrl = null,
title = attachment.title,
artist = attachment.artist,
duration = SimpleDateFormat(
"mm:ss",
Locale.getDefault()
).format(attachment.duration)
)
}
AttachmentType.FILE -> {
val attachment = this.attachment as VkFileDomain
UiChatMaterial.File(
title = attachment.title
)
}
AttachmentType.LINK -> {
val attachment = this.attachment as VkLinkDomain
UiChatMaterial.Link(
title = attachment.title ?: attachment.url,
previewUrl = attachment.photo?.getMaxSize()?.url
)
}
else -> throw IllegalArgumentException("Unsupported type: $type")
}
@@ -27,6 +27,7 @@ import com.meloda.app.fast.common.UserConfig
import com.meloda.app.fast.conversations.model.ConversationOption
import com.meloda.app.fast.conversations.model.ConversationsScreenState
import com.meloda.app.fast.conversations.model.UiConversation
import com.meloda.app.fast.designsystem.LocalBottomPadding
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -45,6 +46,8 @@ fun ConversationsListComposable(
val conversations = screenState.conversations
val bottomPadding = LocalBottomPadding.current
LazyColumn(
modifier = modifier,
state = state
@@ -105,5 +108,9 @@ fun ConversationsListComposable(
}
}
}
item {
Spacer(modifier = Modifier.height(bottomPadding))
}
}
}
@@ -5,16 +5,20 @@ import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideIn
import androidx.compose.animation.slideOut
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
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.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBars
@@ -61,6 +65,7 @@ import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import androidx.core.view.HapticFeedbackConstantsCompat
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coil.imageLoader
@@ -70,6 +75,8 @@ import com.meloda.app.fast.conversations.ConversationsViewModel
import com.meloda.app.fast.conversations.ConversationsViewModelImpl
import com.meloda.app.fast.conversations.model.ConversationsScreenState
import com.meloda.app.fast.conversations.model.UiConversation
import com.meloda.app.fast.designsystem.LocalBottomPadding
import com.meloda.app.fast.designsystem.LocalHazeState
import com.meloda.app.fast.designsystem.LocalTheme
import com.meloda.app.fast.designsystem.MaterialDialog
import com.meloda.app.fast.designsystem.components.FullScreenLoader
@@ -142,7 +149,8 @@ fun ConversationsScreen(
}
}
val hazeState = remember { HazeState() }
// val hazeState = remember { HazeState() }
val hazeState = LocalHazeState.current
var dropDownMenuExpanded by remember {
mutableStateOf(false)
@@ -252,38 +260,42 @@ fun ConversationsScreen(
val scope = rememberCoroutineScope()
val rotation = remember { Animatable(0f) }
AnimatedVisibility(
visible = isListScrollingUp,
modifier = Modifier.navigationBarsPadding(),
enter = slideIn { IntOffset(0, 400) },
exit = slideOut { IntOffset(0, 400) }
) {
FloatingActionButton(
onClick = {
view.performHapticFeedback(HapticFeedbackConstantsCompat.REJECT)
Column {
AnimatedVisibility(
visible = isListScrollingUp,
modifier = Modifier.navigationBarsPadding(),
enter = slideIn { IntOffset(0, 600) } + fadeIn(tween(200)),
exit = slideOut { IntOffset(0, 600) } + fadeOut(tween(200))
) {
FloatingActionButton(
onClick = {
view.performHapticFeedback(HapticFeedbackConstantsCompat.REJECT)
scope.launch {
for (i in 20 downTo 0 step 4) {
rotation.animateTo(
targetValue = i.toFloat(),
animationSpec = tween(50)
)
if (i > 0) {
scope.launch {
for (i in 20 downTo 0 step 4) {
rotation.animateTo(
targetValue = -i.toFloat(),
targetValue = i.toFloat(),
animationSpec = tween(50)
)
if (i > 0) {
rotation.animateTo(
targetValue = -i.toFloat(),
animationSpec = tween(50)
)
}
}
}
}
},
modifier = Modifier.rotate(rotation.value)
) {
Icon(
painter = painterResource(id = UiR.drawable.ic_baseline_create_24),
contentDescription = "Add chat button"
)
},
modifier = Modifier.rotate(rotation.value)
) {
Icon(
painter = painterResource(id = UiR.drawable.ic_baseline_create_24),
contentDescription = "Add chat button"
)
}
}
Spacer(modifier = Modifier.height(LocalBottomPadding.current))
}
}
) { padding ->
@@ -77,7 +77,7 @@ class FriendsViewModelImpl(
}
private fun loadFriends(offset: Int = currentOffset.value) {
friendsUseCase.getAllFriends(count = 30, offset = offset).listenValue { state ->
friendsUseCase.getAllFriends(count = LOAD_COUNT, offset = offset).listenValue { state ->
state.processState(
error = { error ->
when (error) {
@@ -103,11 +103,11 @@ class FriendsViewModelImpl(
},
success = { info ->
val response = info.friends
val itemsCountSufficient = response.size == 30
val itemsCountSufficient = response.size == LOAD_COUNT
canPaginate.setValue { itemsCountSufficient }
val paginationExhausted = !itemsCountSufficient &&
screenState.value.friends.size >= 30
screenState.value.friends.size >= LOAD_COUNT
imagesToPreload.setValue {
response.mapNotNull(VkUser::photo100)
@@ -172,4 +172,8 @@ class FriendsViewModelImpl(
}
uiOnlineFriends.setValue { onlineUiFriends }
}
companion object {
const val LOAD_COUNT = 30
}
}
@@ -354,6 +354,7 @@ class MessagesHistoryViewModelImpl(
val newMessage = VkMessage(
id = -1 - sendingMessages.size,
conversationMessageId = -1,
text = lastMessageText,
isOut = true,
peerId = screenState.value.conversationId,
@@ -1,27 +1,28 @@
package com.meloda.app.fast.messageshistory.domain
import com.meloda.app.fast.data.State
import com.meloda.app.fast.data.api.messages.MessagesHistoryDomain
import com.meloda.app.fast.data.api.messages.MessagesHistoryInfo
import com.meloda.app.fast.data.api.messages.MessagesRepository
import com.meloda.app.fast.data.api.messages.MessagesUseCase
import com.meloda.app.fast.data.mapToState
import com.meloda.app.fast.model.api.domain.VkAttachment
import com.meloda.app.fast.model.api.domain.VkAttachmentHistoryMessage
import com.meloda.app.fast.model.api.domain.VkMessage
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
class MessagesUseCaseImpl(
private val messagesRepository: MessagesRepository
private val repository: MessagesRepository
) : MessagesUseCase {
override fun getMessagesHistory(
conversationId: Int,
count: Int?,
offset: Int?
): Flow<State<MessagesHistoryDomain>> = flow {
): Flow<State<MessagesHistoryInfo>> = flow {
emit(State.Loading)
val newState = messagesRepository.getMessagesHistory(
val newState = repository.getHistory(
conversationId = conversationId,
offset = offset,
count = count
@@ -31,60 +32,20 @@ class MessagesUseCaseImpl(
}
override fun getById(
messageId: Int,
extended: Boolean?,
fields: String?
): Flow<State<VkMessage?>> = flow {
emit(State.Loading)
val newState = messagesRepository.getMessageById(
messagesIds = listOf(messageId),
extended = extended,
fields = fields
).mapToState()
emit(newState)
}
override fun getByIds(
messageIds: List<Int>,
extended: Boolean?,
fields: String?
): Flow<State<List<VkMessage>>> = flow {}
// flow {
// emit(State.Loading)
//
// val newState = messagesRepository.getById(
// params = MessagesGetByIdRequest(
// messagesIds = messageIds,
// extended = extended,
// fields = fields
// )
// ).fold(
// onSuccess = { response ->
// val messages = response.items
// val usersMap =
// VkUsersMap.forUsers(response.profiles.orEmpty().map(VkUserData::mapToDomain))
// val groupsMap =
// VkGroupsMap.forGroups(response.groups.orEmpty().map(VkGroupData::mapToDomain))
//
// com.meloda.app.fast.network.State.Success(
// messages.map { message ->
// message.mapToDomain(
// user = usersMap.messageUser(message),
// group = groupsMap.messageGroup(message),
// actionUser = usersMap.messageActionUser(message),
// actionGroup = groupsMap.messageActionGroup(message)
// )
// }
// )
// },
// onNetworkFailure = { com.meloda.app.fast.network.State.Error.ConnectionError },
// onUnknownFailure = { com.meloda.app.fast.network.State.UNKNOWN_ERROR },
// onHttpFailure = { result -> result.error.toStateApiError() },
// onApiFailure = { result -> result.error.toStateApiError() }
// )
// emit(newState)
// }
): Flow<State<List<VkMessage>>> = flow {
emit(State.Loading)
val newState = repository.getById(
messagesIds = messageIds,
extended = extended,
fields = fields
).mapToState()
emit(newState)
}
override fun sendMessage(
peerId: Int,
@@ -95,7 +56,7 @@ class MessagesUseCaseImpl(
): Flow<State<Int>> = flow {
emit(State.Loading)
val newState = messagesRepository.send(
val newState = repository.send(
peerId = peerId,
randomId = randomId,
message = message,
@@ -112,7 +73,7 @@ class MessagesUseCaseImpl(
): Flow<State<Int>> = flow {
emit(State.Loading)
val newState = messagesRepository.markAsRead(
val newState = repository.markAsRead(
peerId = peerId,
startMessageId = startMessageId
).mapToState()
@@ -120,11 +81,31 @@ class MessagesUseCaseImpl(
emit(newState)
}
override fun getHistoryAttachments(
peerId: Int,
count: Int?,
offset: Int?,
attachmentTypes: List<String>,
conversationMessageId: Int
): Flow<State<List<VkAttachmentHistoryMessage>>> = flow {
emit(State.Loading)
val newState = repository.getHistoryAttachments(
peerId = peerId,
count = count,
offset = offset,
attachmentTypes = attachmentTypes,
conversationMessageId = conversationMessageId
).mapToState()
emit(newState)
}
override suspend fun storeMessage(message: VkMessage) {
messagesRepository.storeMessages(listOf(message))
repository.storeMessages(listOf(message))
}
override suspend fun storeMessages(messages: List<VkMessage>) {
messagesRepository.storeMessages(messages)
repository.storeMessages(messages)
}
}
@@ -4,6 +4,7 @@ import com.meloda.app.fast.common.UiImage
data class UiMessage(
val id: Int,
val conversationMessageId: Int,
val text: String?,
val isOut: Boolean,
val fromId: Int,
@@ -40,7 +40,7 @@ val MessagesHistoryNavType = object : NavType<MessagesHistoryArguments>(isNullab
fun NavGraphBuilder.messagesHistoryRoute(
onError: (BaseError) -> Unit,
onBack: () -> Unit,
onNavigateToChatAttachments: () -> Unit
onNavigateToChatAttachments: (peerId: Int, conversationMessageId: Int) -> Unit
) {
composable<MessagesHistory>(
typeMap = mapOf(typeOf<MessagesHistoryArguments>() to MessagesHistoryNavType)
@@ -90,7 +90,7 @@ import com.meloda.app.fast.designsystem.R as UiR
fun MessagesHistoryScreen(
onError: (BaseError) -> Unit,
onBack: () -> Unit,
onNavigateToChatMaterials: () -> Unit,
onNavigateToChatMaterials: (peerId: Int, conversationMessageId: Int) -> Unit,
viewModel: MessagesHistoryViewModel = koinViewModel<MessagesHistoryViewModelImpl>()
) {
val view = LocalView.current
@@ -215,7 +215,12 @@ fun MessagesHistoryScreen(
DropdownMenuItem(
onClick = {
dropDownMenuExpanded = false
onNavigateToChatMaterials()
// TODO: 11/07/2024, Danil Nikolaev: to VM
onNavigateToChatMaterials(
screenState.conversationId,
screenState.messages.first().conversationMessageId
)
},
text = {
Text(text = "Materials")
@@ -91,6 +91,7 @@ fun VkMessage.asPresentation(
nextMessage: VkMessage?
): UiMessage = UiMessage(
id = id,
conversationMessageId = conversationMessageId,
text = text,
isOut = isOut,
fromId = fromId,