From b80babed9c3b93a4b6b77ac3b1c35ef1c564e173 Mon Sep 17 00:00:00 2001 From: Danil Nikolaev Date: Thu, 27 Mar 2025 04:32:11 +0300 Subject: [PATCH] draft pinned message and fixes --- .../MessagesHistoryViewModel.kt | 88 ++++++++++++++---- .../presentation/MessagesHistoryScreen.kt | 92 ++++++++++++++++--- .../presentation/MessagesList.kt | 5 + 3 files changed, 158 insertions(+), 27 deletions(-) diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryViewModel.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryViewModel.kt index 2c6af89c..3d9762e1 100644 --- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryViewModel.kt +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryViewModel.kt @@ -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.sorted(): List { return sortedWith { m1, m2 -> val dateDiff = m2.date - m1.date diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesHistoryScreen.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesHistoryScreen.kt index 5fd03358..624e66a4 100644 --- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesHistoryScreen.kt +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesHistoryScreen.kt @@ -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, diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesList.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesList.kt index 8da688dd..6134fd3d 100644 --- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesList.kt +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesList.kt @@ -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, @@ -168,6 +169,10 @@ fun MessagesList( } } + if (hasPinnedMessage) { + Spacer(modifier = Modifier.height(56.dp)) + } + Spacer(Modifier.height(8.dp)) Spacer( modifier = Modifier