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.setValue
import dev.meloda.fast.common.provider.ResourceProvider
import dev.meloda.fast.data.State
import dev.meloda.fast.data.UserConfig
import dev.meloda.fast.data.VkMemoryCache
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.api.domain.VkAttachment
import dev.meloda.fast.model.api.domain.VkMessage
import dev.meloda.fast.network.VkErrorCode
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -75,6 +77,7 @@ class MessagesHistoryViewModelImpl(
private val conversationsUseCase: ConversationsUseCase,
private val resourceProvider: ResourceProvider,
private val userSettings: UserSettings,
private val loadConversationsByIdUseCase: LoadConversationsByIdUseCase,
updatesParser: LongPollUpdatesParser,
savedStateHandle: SavedStateHandle
) : MessagesHistoryViewModel, ViewModel() {
@@ -99,6 +102,8 @@ class MessagesHistoryViewModelImpl(
val arguments = MessagesHistory.from(savedStateHandle).arguments
screenState.setValue { old -> old.copy(conversationId = arguments.conversationId) }
loadConversation()
loadMessagesHistory()
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) {
Log.d("MessagesHistoryViewModel", "loadMessagesHistory: $offset")
@@ -374,7 +406,7 @@ class MessagesHistoryViewModelImpl(
offset = offset,
).listenValue(viewModelScope) { state ->
state.processState(
error = { error -> },
error = ::handleError,
success = { response ->
val messages = response.messages
val fullMessages = if (offset == 0) {
@@ -397,24 +429,10 @@ class MessagesHistoryViewModelImpl(
val paginationExhausted = !itemsCountSufficient &&
screenState.value.messages.isNotEmpty()
var newState = screenState.value.copy(
val newState = screenState.value.copy(
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 ->
message.asPresentation(
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> {
return sortedWith { m1, m2 ->
val dateDiff = m2.date - m1.date
@@ -2,12 +2,15 @@ package dev.meloda.fast.messageshistory.presentation
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.FastOutLinearInEasing
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
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.res.painterResource
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.style.TextOverflow
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.indexOfMessageByCmId
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.IconButton
import dev.meloda.fast.ui.theme.LocalThemeConfig
@@ -163,9 +169,7 @@ fun MessagesHistoryScreen(
onMessageLongClicked: (Int) -> Unit = {}
) {
val view = LocalView.current
val coroutineScope = rememberCoroutineScope()
val currentTheme = LocalThemeConfig.current
BackHandler(
@@ -173,6 +177,12 @@ fun MessagesHistoryScreen(
onBack = onClose
)
val pinnedMessage by remember(screenState) {
derivedStateOf {
screenState.conversation.pinnedMessage
}
}
val listState = rememberLazyListState()
val paginationConditionMet by remember(canPaginate, listState) {
@@ -195,10 +205,24 @@ fun MessagesHistoryScreen(
val hazeState = remember { HazeState() }
val toolbarColorAlpha by animateFloatAsState(
targetValue = if (!listState.canScrollForward) 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 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 {
@@ -215,7 +239,19 @@ fun MessagesHistoryScreen(
modifier = Modifier.fillMaxSize(),
contentWindowInsets = WindowInsets.statusBars,
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(
modifier = Modifier
.then(
@@ -285,11 +321,7 @@ fun MessagesHistoryScreen(
)
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.surface.copy(
alpha = if (currentTheme.enableBlur) toolbarColorAlpha else 1f
)
),
colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent),
actions = {
if (selectedMessages.isNotEmpty()) {
AnimatedVisibility(showReplyAction) {
@@ -402,6 +434,43 @@ fun MessagesHistoryScreen(
AnimatedVisibility(!showHorizontalProgressBar) {
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 ->
@@ -415,6 +484,7 @@ fun MessagesHistoryScreen(
MessagesList(
hazeState = hazeState,
listState = listState,
hasPinnedMessage = pinnedMessage != null,
immutableMessages = ImmutableList.copyOf(screenState.messages),
isPaginating = screenState.isPaginating,
messageBarHeight = messageBarHeight,
@@ -39,6 +39,7 @@ import dev.meloda.fast.ui.util.ImmutableList
@Composable
fun MessagesList(
modifier: Modifier = Modifier,
hasPinnedMessage: Boolean,
hazeState: HazeState,
listState: LazyListState,
immutableMessages: ImmutableList<UiItem>,
@@ -168,6 +169,10 @@ fun MessagesList(
}
}
if (hasPinnedMessage) {
Spacer(modifier = Modifier.height(56.dp))
}
Spacer(Modifier.height(8.dp))
Spacer(
modifier = Modifier