forked from melod1n/fast-messenger
draft pinned message and fixes
This commit is contained in:
+72
-16
@@ -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
|
||||
|
||||
+81
-11
@@ -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,
|
||||
|
||||
+5
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user