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.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
|
||||||
|
|||||||
+81
-11
@@ -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,
|
||||||
|
|||||||
+5
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user