draft pinned message and fixes

This commit is contained in:
2025-03-27 04:32:11 +03:00
parent 85c5a10891
commit b80babed9c
3 changed files with 158 additions and 27 deletions
@@ -15,6 +15,7 @@ import com.conena.nanokt.text.isNotEmptyOrBlank
import dev.meloda.fast.common.extensions.listenValue import dev.meloda.fast.common.extensions.listenValue
import dev.meloda.fast.common.extensions.setValue import dev.meloda.fast.common.extensions.setValue
import dev.meloda.fast.common.provider.ResourceProvider import dev.meloda.fast.common.provider.ResourceProvider
import dev.meloda.fast.data.State
import dev.meloda.fast.data.UserConfig import dev.meloda.fast.data.UserConfig
import dev.meloda.fast.data.VkMemoryCache import dev.meloda.fast.data.VkMemoryCache
import dev.meloda.fast.data.processState import dev.meloda.fast.data.processState
@@ -37,6 +38,7 @@ import dev.meloda.fast.model.BaseError
import dev.meloda.fast.model.LongPollParsedEvent import dev.meloda.fast.model.LongPollParsedEvent
import dev.meloda.fast.model.api.domain.VkAttachment import dev.meloda.fast.model.api.domain.VkAttachment
import dev.meloda.fast.model.api.domain.VkMessage import dev.meloda.fast.model.api.domain.VkMessage
import dev.meloda.fast.network.VkErrorCode
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
@@ -75,6 +77,7 @@ class MessagesHistoryViewModelImpl(
private val conversationsUseCase: ConversationsUseCase, private val conversationsUseCase: ConversationsUseCase,
private val resourceProvider: ResourceProvider, private val resourceProvider: ResourceProvider,
private val userSettings: UserSettings, private val userSettings: UserSettings,
private val loadConversationsByIdUseCase: LoadConversationsByIdUseCase,
updatesParser: LongPollUpdatesParser, updatesParser: LongPollUpdatesParser,
savedStateHandle: SavedStateHandle savedStateHandle: SavedStateHandle
) : MessagesHistoryViewModel, ViewModel() { ) : MessagesHistoryViewModel, ViewModel() {
@@ -99,6 +102,8 @@ class MessagesHistoryViewModelImpl(
val arguments = MessagesHistory.from(savedStateHandle).arguments val arguments = MessagesHistory.from(savedStateHandle).arguments
screenState.setValue { old -> old.copy(conversationId = arguments.conversationId) } screenState.setValue { old -> old.copy(conversationId = arguments.conversationId) }
loadConversation()
loadMessagesHistory() loadMessagesHistory()
updatesParser.onNewMessage(::handleNewMessage) updatesParser.onNewMessage(::handleNewMessage)
@@ -365,6 +370,33 @@ class MessagesHistoryViewModelImpl(
} }
} }
private fun loadConversation() {
Log.d("MessagesHistoryViewModelImpl", "loadConversation()")
loadConversationsByIdUseCase(listOf(screenState.value.conversationId))
.listenValue(viewModelScope) { state ->
state.processState(
error = ::handleError,
success = { response ->
val conversation = response.firstOrNull() ?: return@listenValue
screenState.setValue { old ->
old.copy(conversation = conversation)
}
screenState.setValue { old ->
old.copy(
title = conversation.extractTitle(
useContactName = AppSettings.General.useContactNames,
resources = resourceProvider.resources
),
avatar = conversation.extractAvatar(),
conversation = conversation
)
}
}
)
}
}
private fun loadMessagesHistory(offset: Int = currentOffset.value) { private fun loadMessagesHistory(offset: Int = currentOffset.value) {
Log.d("MessagesHistoryViewModel", "loadMessagesHistory: $offset") Log.d("MessagesHistoryViewModel", "loadMessagesHistory: $offset")
@@ -374,7 +406,7 @@ class MessagesHistoryViewModelImpl(
offset = offset, offset = offset,
).listenValue(viewModelScope) { state -> ).listenValue(viewModelScope) { state ->
state.processState( state.processState(
error = { error -> }, error = ::handleError,
success = { response -> success = { response ->
val messages = response.messages val messages = response.messages
val fullMessages = if (offset == 0) { val fullMessages = if (offset == 0) {
@@ -397,24 +429,10 @@ class MessagesHistoryViewModelImpl(
val paginationExhausted = !itemsCountSufficient && val paginationExhausted = !itemsCountSufficient &&
screenState.value.messages.isNotEmpty() screenState.value.messages.isNotEmpty()
var newState = screenState.value.copy( val newState = screenState.value.copy(
isPaginationExhausted = paginationExhausted, isPaginationExhausted = paginationExhausted,
) )
conversations
.firstOrNull { it.id == screenState.value.conversationId }
?.let { conversation ->
screenState.setValue { old -> old.copy(conversation = conversation) }
newState = newState.copy(
title = conversation.extractTitle(
useContactName = AppSettings.General.useContactNames,
resources = resourceProvider.resources
),
avatar = conversation.extractAvatar(),
conversation = conversation
)
}
val loadedMessages = fullMessages.mapIndexed { index, message -> val loadedMessages = fullMessages.mapIndexed { index, message ->
message.asPresentation( message.asPresentation(
resourceProvider = resourceProvider, resourceProvider = resourceProvider,
@@ -441,6 +459,44 @@ class MessagesHistoryViewModelImpl(
} }
} }
private fun handleError(error: State.Error) {
when (error) {
is State.Error.ApiError -> {
when (error.errorCode) {
VkErrorCode.USER_AUTHORIZATION_FAILED -> {
baseError.setValue { BaseError.SessionExpired }
}
else -> {
baseError.setValue {
BaseError.SimpleError(message = error.errorMessage)
}
}
}
}
State.Error.ConnectionError -> {
baseError.setValue {
BaseError.SimpleError(message = "Connection error")
}
}
State.Error.InternalError -> {
baseError.setValue {
BaseError.SimpleError(message = "Internal error")
}
}
State.Error.UnknownError -> {
baseError.setValue {
BaseError.SimpleError(message = "Unknown error")
}
}
else -> Unit
}
}
private fun List<VkMessage>.sorted(): List<VkMessage> { private fun List<VkMessage>.sorted(): List<VkMessage> {
return sortedWith { m1, m2 -> return sortedWith { m1, m2 ->
val dateDiff = m2.date - m1.date val dateDiff = m2.date - m1.date
@@ -2,12 +2,15 @@ package dev.meloda.fast.messageshistory.presentation
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.animateContentSize import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.FastOutLinearInEasing
import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@@ -70,6 +73,7 @@ import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.DpOffset
@@ -91,6 +95,8 @@ import dev.meloda.fast.messageshistory.model.MessagesHistoryScreenState
import dev.meloda.fast.messageshistory.util.firstMessage import dev.meloda.fast.messageshistory.util.firstMessage
import dev.meloda.fast.messageshistory.util.indexOfMessageByCmId import dev.meloda.fast.messageshistory.util.indexOfMessageByCmId
import dev.meloda.fast.model.BaseError import dev.meloda.fast.model.BaseError
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.ErrorView
import dev.meloda.fast.ui.components.IconButton import dev.meloda.fast.ui.components.IconButton
import dev.meloda.fast.ui.theme.LocalThemeConfig import dev.meloda.fast.ui.theme.LocalThemeConfig
@@ -163,9 +169,7 @@ fun MessagesHistoryScreen(
onMessageLongClicked: (Int) -> Unit = {} onMessageLongClicked: (Int) -> Unit = {}
) { ) {
val view = LocalView.current val view = LocalView.current
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
val currentTheme = LocalThemeConfig.current val currentTheme = LocalThemeConfig.current
BackHandler( BackHandler(
@@ -173,6 +177,12 @@ fun MessagesHistoryScreen(
onBack = onClose onBack = onClose
) )
val pinnedMessage by remember(screenState) {
derivedStateOf {
screenState.conversation.pinnedMessage
}
}
val listState = rememberLazyListState() val listState = rememberLazyListState()
val paginationConditionMet by remember(canPaginate, listState) { val paginationConditionMet by remember(canPaginate, listState) {
@@ -195,10 +205,24 @@ fun MessagesHistoryScreen(
val hazeState = remember { HazeState() } val hazeState = remember { HazeState() }
val toolbarColorAlpha by animateFloatAsState( val topBarContainerColorAlpha by animateFloatAsState(
targetValue = if (!listState.canScrollForward) 1f else 0f, targetValue = if (!currentTheme.enableBlur || !listState.canScrollBackward) 1f else 0f,
label = "toolbarColorAlpha", label = "toolbarColorAlpha",
animationSpec = tween(durationMillis = 50) animationSpec = tween(
durationMillis = 200,
easing = FastOutLinearInEasing
)
)
val topBarContainerColor by animateColorAsState(
targetValue =
if (currentTheme.enableBlur || !listState.canScrollBackward) MaterialTheme.colorScheme.surface
else MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp),
label = "toolbarColorAlpha",
animationSpec = tween(
durationMillis = 200,
easing = FastOutLinearInEasing
)
) )
var messageBarHeight by remember { var messageBarHeight by remember {
@@ -215,7 +239,19 @@ fun MessagesHistoryScreen(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentWindowInsets = WindowInsets.statusBars, contentWindowInsets = WindowInsets.statusBars,
topBar = { topBar = {
Column(modifier = Modifier.fillMaxWidth()) { Column(
modifier = Modifier
.fillMaxWidth()
.background(topBarContainerColor.copy(alpha = topBarContainerColorAlpha))
.then(
if (currentTheme.enableBlur) {
Modifier.hazeEffect(
state = hazeState,
style = HazeMaterials.thick()
)
} else Modifier
)
) {
TopAppBar( TopAppBar(
modifier = Modifier modifier = Modifier
.then( .then(
@@ -285,11 +321,7 @@ fun MessagesHistoryScreen(
) )
} }
}, },
colors = TopAppBarDefaults.topAppBarColors( colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent),
containerColor = MaterialTheme.colorScheme.surface.copy(
alpha = if (currentTheme.enableBlur) toolbarColorAlpha else 1f
)
),
actions = { actions = {
if (selectedMessages.isNotEmpty()) { if (selectedMessages.isNotEmpty()) {
AnimatedVisibility(showReplyAction) { AnimatedVisibility(showReplyAction) {
@@ -402,6 +434,43 @@ fun MessagesHistoryScreen(
AnimatedVisibility(!showHorizontalProgressBar) { AnimatedVisibility(!showHorizontalProgressBar) {
HorizontalDivider() HorizontalDivider()
} }
if (!screenState.isLoading && pinnedMessage != null) {
Row(
modifier = Modifier
.fillMaxWidth()
.height(56.dp)
.clickable {
}
.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
modifier = Modifier.rotate(45f),
painter = painterResource(UiR.drawable.ic_round_push_pin_24),
contentDescription = null
)
Spacer(modifier = Modifier.width(16.dp))
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = pinnedMessage?.user?.toString()
?: pinnedMessage?.group?.name
?: "...",
fontWeight = FontWeight.Medium,
color = MaterialTheme.colorScheme.primary
)
LocalContentAlpha(alpha = ContentAlpha.medium) {
Text(text = pinnedMessage?.text.orEmpty())
}
}
}
HorizontalDivider()
}
} }
} }
) { padding -> ) { padding ->
@@ -415,6 +484,7 @@ fun MessagesHistoryScreen(
MessagesList( MessagesList(
hazeState = hazeState, hazeState = hazeState,
listState = listState, listState = listState,
hasPinnedMessage = pinnedMessage != null,
immutableMessages = ImmutableList.copyOf(screenState.messages), immutableMessages = ImmutableList.copyOf(screenState.messages),
isPaginating = screenState.isPaginating, isPaginating = screenState.isPaginating,
messageBarHeight = messageBarHeight, messageBarHeight = messageBarHeight,
@@ -39,6 +39,7 @@ import dev.meloda.fast.ui.util.ImmutableList
@Composable @Composable
fun MessagesList( fun MessagesList(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
hasPinnedMessage: Boolean,
hazeState: HazeState, hazeState: HazeState,
listState: LazyListState, listState: LazyListState,
immutableMessages: ImmutableList<UiItem>, immutableMessages: ImmutableList<UiItem>,
@@ -168,6 +169,10 @@ fun MessagesList(
} }
} }
if (hasPinnedMessage) {
Spacer(modifier = Modifier.height(56.dp))
}
Spacer(Modifier.height(8.dp)) Spacer(Modifier.height(8.dp))
Spacer( Spacer(
modifier = Modifier modifier = Modifier