forked from melod1n/fast-messenger
pinned message in messages history draft
This commit is contained in:
+1
-1
@@ -170,6 +170,6 @@ class ChatMaterialsViewModelImpl(
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val LOAD_COUNT = 200
|
||||
const val LOAD_COUNT = 100
|
||||
}
|
||||
}
|
||||
|
||||
+80
-7
@@ -1,8 +1,6 @@
|
||||
package dev.meloda.fast.messageshistory
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.compose.ui.text.TextRange
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
@@ -54,13 +52,18 @@ interface MessagesHistoryViewModel {
|
||||
val screenState: StateFlow<MessagesHistoryScreenState>
|
||||
val selectedMessages: StateFlow<List<Int>>
|
||||
|
||||
val isNeedToScrollToIndex: StateFlow<Int?>
|
||||
|
||||
val baseError: StateFlow<BaseError?>
|
||||
val imagesToPreload: StateFlow<List<String>>
|
||||
|
||||
val currentOffset: StateFlow<Int>
|
||||
val showMessageOptions: StateFlow<VkMessage?>
|
||||
|
||||
val currentOffset: StateFlow<Int>
|
||||
val canPaginate: StateFlow<Boolean>
|
||||
|
||||
fun onScrolledToIndex()
|
||||
|
||||
fun onCloseButtonClicked()
|
||||
fun onRefresh()
|
||||
fun onAttachmentButtonClicked()
|
||||
@@ -72,10 +75,12 @@ interface MessagesHistoryViewModel {
|
||||
|
||||
fun onMessageClicked(messageId: Int)
|
||||
fun onMessageLongClicked(messageId: Int)
|
||||
fun onMessageOptionsDialogDismissed()
|
||||
fun onPinnedMessageClicked(messageId: Int)
|
||||
fun onUnpinMessageClicked()
|
||||
}
|
||||
|
||||
class MessagesHistoryViewModelImpl(
|
||||
private val applicationContext: Context,
|
||||
private val messagesUseCase: MessagesUseCase,
|
||||
private val conversationsUseCase: ConversationsUseCase,
|
||||
private val resourceProvider: ResourceProvider,
|
||||
@@ -88,9 +93,13 @@ class MessagesHistoryViewModelImpl(
|
||||
override val screenState = MutableStateFlow(MessagesHistoryScreenState.EMPTY)
|
||||
override val selectedMessages = MutableStateFlow<List<Int>>(emptyList())
|
||||
|
||||
override val isNeedToScrollToIndex = MutableStateFlow<Int?>(null)
|
||||
|
||||
override val baseError = MutableStateFlow<BaseError?>(null)
|
||||
override val imagesToPreload = MutableStateFlow<List<String>>(emptyList())
|
||||
|
||||
override val showMessageOptions = MutableStateFlow<VkMessage?>(null)
|
||||
|
||||
override val currentOffset = MutableStateFlow(0)
|
||||
|
||||
override val canPaginate = MutableStateFlow(false)
|
||||
@@ -120,6 +129,10 @@ class MessagesHistoryViewModelImpl(
|
||||
)
|
||||
}
|
||||
|
||||
override fun onScrolledToIndex() {
|
||||
isNeedToScrollToIndex.setValue { null }
|
||||
}
|
||||
|
||||
override fun onCloseButtonClicked() {
|
||||
screenState.setValue { old ->
|
||||
old.copy(
|
||||
@@ -211,7 +224,9 @@ class MessagesHistoryViewModelImpl(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(applicationContext, "Click", Toast.LENGTH_SHORT).show()
|
||||
messages.value.firstOrNull { it.id == currentMessage.id }?.let { message ->
|
||||
showMessageOptions.setValue { message }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,6 +255,62 @@ class MessagesHistoryViewModelImpl(
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMessageOptionsDialogDismissed() {
|
||||
showMessageOptions.setValue { null }
|
||||
}
|
||||
|
||||
override fun onPinnedMessageClicked(messageId: Int) {
|
||||
val messageIndex = screenState.value.messages.indexOfFirstOrNull {
|
||||
it is UiItem.Message && it.id == messageId
|
||||
}
|
||||
|
||||
if (messageIndex == null) { // сообщения нет в списке
|
||||
// pizdets
|
||||
} else {
|
||||
isNeedToScrollToIndex.setValue { messageIndex }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onUnpinMessageClicked() {
|
||||
// TODO: 27.03.2025, Danil Nikolaev: confirmation alert
|
||||
val pinnedMessageId = screenState.value.pinnedMessage?.id ?: return
|
||||
unpinMessage(pinnedMessageId)
|
||||
}
|
||||
|
||||
private fun unpinMessage(messageId: Int) {
|
||||
val messageIndex = screenState.value.messages.indexOfFirstOrNull {
|
||||
it is UiItem.Message && it.id == messageId
|
||||
}
|
||||
|
||||
messagesUseCase.unpin(screenState.value.conversationId)
|
||||
.listenValue(viewModelScope) { state ->
|
||||
state.processState(
|
||||
error = ::handleError,
|
||||
success = {
|
||||
var newState = screenState.value.copy(
|
||||
pinnedMessage = null,
|
||||
conversation = screenState.value.conversation.copy(
|
||||
pinnedMessage = null,
|
||||
pinnedMessageId = null
|
||||
),
|
||||
pinnedSummary = null,
|
||||
pinnedTitle = null
|
||||
)
|
||||
|
||||
if (messageIndex != null) {
|
||||
val newMessages = screenState.value.messages.toMutableList()
|
||||
val currentMessage: UiItem.Message =
|
||||
newMessages[messageIndex] as UiItem.Message
|
||||
newMessages[messageIndex] = currentMessage.copy(isPinned = false)
|
||||
newState = newState.copy(messages = newMessages)
|
||||
}
|
||||
|
||||
screenState.setValue { old -> newState }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleNewMessage(event: LongPollParsedEvent.NewMessage) {
|
||||
val message = event.message
|
||||
|
||||
@@ -543,7 +614,7 @@ class MessagesHistoryViewModelImpl(
|
||||
actionConversationMessageId = null,
|
||||
actionMessage = null,
|
||||
updateTime = null,
|
||||
important = false,
|
||||
isImportant = false,
|
||||
forwards = null,
|
||||
attachments = null,
|
||||
replyMessage = null,
|
||||
@@ -551,7 +622,9 @@ class MessagesHistoryViewModelImpl(
|
||||
user = VkMemoryCache.getUser(UserConfig.userId),
|
||||
group = null,
|
||||
actionUser = null,
|
||||
actionGroup = null
|
||||
actionGroup = null,
|
||||
isPinned = false,
|
||||
pinnedAt = null
|
||||
)
|
||||
sendingMessages += newMessage
|
||||
|
||||
|
||||
+3
-2
@@ -24,8 +24,9 @@ sealed class UiItem(
|
||||
val avatar: UiImage,
|
||||
val isEdited: Boolean,
|
||||
val isRead: Boolean,
|
||||
val sendingStatus: SendingStatus = SendingStatus.SENT,
|
||||
val isSelected: Boolean = false
|
||||
val sendingStatus: SendingStatus,
|
||||
val isSelected: Boolean,
|
||||
val isPinned: Boolean
|
||||
) : UiItem(id, conversationMessageId)
|
||||
|
||||
data class ActionMessage(
|
||||
|
||||
+5
-3
@@ -27,13 +27,15 @@ fun ActionMessageItem(
|
||||
Text(
|
||||
text = item.text,
|
||||
modifier = modifier
|
||||
.padding(horizontal = 32.dp)
|
||||
.padding(
|
||||
horizontal = 32.dp,
|
||||
vertical = 4.dp
|
||||
)
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.then(
|
||||
if (item.actionCmId != null) {
|
||||
Modifier.clickable(onClick = onClick)
|
||||
}
|
||||
else Modifier
|
||||
} else Modifier
|
||||
)
|
||||
.background(MaterialTheme.colorScheme.surfaceColorAtElevation(5.dp))
|
||||
.fillMaxWidth()
|
||||
|
||||
+5
-3
@@ -1,6 +1,7 @@
|
||||
package dev.meloda.fast.messageshistory.presentation
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -33,10 +34,10 @@ fun IncomingMessageBubble(
|
||||
message: UiItem.Message,
|
||||
animate: Boolean,
|
||||
) {
|
||||
Row(modifier = modifier.fillMaxWidth()) {
|
||||
Row(modifier = modifier.fillMaxWidth().then(if (animate) Modifier.animateContentSize() else Modifier),) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(0.75f)
|
||||
.fillMaxWidth(0.85f)
|
||||
.padding(start = 16.dp),
|
||||
verticalAlignment = Alignment.Bottom,
|
||||
horizontalArrangement = Arrangement.Start
|
||||
@@ -82,7 +83,8 @@ fun IncomingMessageBubble(
|
||||
edited = message.isEdited,
|
||||
animate = animate,
|
||||
isRead = message.isRead,
|
||||
sendingStatus = message.sendingStatus
|
||||
sendingStatus = message.sendingStatus,
|
||||
pinned = message.isPinned
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+17
-2
@@ -25,6 +25,7 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -40,7 +41,8 @@ fun MessageBubble(
|
||||
edited: Boolean,
|
||||
animate: Boolean,
|
||||
isRead: Boolean,
|
||||
sendingStatus: SendingStatus
|
||||
sendingStatus: SendingStatus,
|
||||
pinned: Boolean
|
||||
) {
|
||||
val backgroundColor = if (!isOut) {
|
||||
MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)
|
||||
@@ -63,12 +65,14 @@ fun MessageBubble(
|
||||
horizontal = 8.dp,
|
||||
vertical = 6.dp
|
||||
)
|
||||
.then(if (animate) Modifier.animateContentSize() else Modifier),
|
||||
) {
|
||||
val minDateContainerWidth = remember(edited, isOut) {
|
||||
val mainPart = if (edited) 50.dp else 30.dp
|
||||
val readIndicatorPart = if (isOut) 14.dp else 0.dp
|
||||
val pinnedIndicatorPart = if (pinned) 14.dp else 0.dp
|
||||
|
||||
mainPart + readIndicatorPart
|
||||
mainPart + readIndicatorPart + pinnedIndicatorPart
|
||||
}
|
||||
|
||||
val dateContainerWidth by animateDpAsState(
|
||||
@@ -94,7 +98,18 @@ fun MessageBubble(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomEnd)
|
||||
.defaultMinSize(minWidth = dateContainerWidth)
|
||||
.then(if (animate) Modifier.animateContentSize() else Modifier),
|
||||
) {
|
||||
if (pinned) {
|
||||
Icon(
|
||||
painter = painterResource(UiR.drawable.ic_round_push_pin_24),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(14.dp)
|
||||
.rotate(45f)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
}
|
||||
if (edited) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Create,
|
||||
|
||||
+75
-6
@@ -64,6 +64,7 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.graphics.Color
|
||||
@@ -100,6 +101,8 @@ 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.components.MaterialDialog
|
||||
import dev.meloda.fast.ui.components.SelectionType
|
||||
import dev.meloda.fast.ui.theme.LocalThemeConfig
|
||||
import dev.meloda.fast.ui.util.ImmutableList
|
||||
import dev.meloda.fast.ui.util.getImage
|
||||
@@ -119,18 +122,22 @@ fun MessagesHistoryRoute(
|
||||
val selectedMessages by viewModel.selectedMessages.collectAsStateWithLifecycle()
|
||||
val baseError by viewModel.baseError.collectAsStateWithLifecycle()
|
||||
val canPaginate by viewModel.canPaginate.collectAsStateWithLifecycle()
|
||||
val showMessageOptions by viewModel.showMessageOptions.collectAsStateWithLifecycle()
|
||||
val scrollIndex by viewModel.isNeedToScrollToIndex.collectAsStateWithLifecycle()
|
||||
|
||||
val userSettings: UserSettings = koinInject()
|
||||
val showEmojiButton by userSettings.showEmojiButton.collectAsStateWithLifecycle()
|
||||
|
||||
MessagesHistoryScreen(
|
||||
screenState = screenState,
|
||||
scrollIndex = scrollIndex,
|
||||
selectedMessages = ImmutableList.copyOf(selectedMessages),
|
||||
baseError = baseError,
|
||||
canPaginate = canPaginate,
|
||||
showEmojiButton = showEmojiButton,
|
||||
onBack = onBack,
|
||||
onClose = viewModel::onCloseButtonClicked,
|
||||
onScrolledToIndex = viewModel::onScrolledToIndex,
|
||||
onSessionExpiredLogOutButtonClicked = { onError(BaseError.SessionExpired) },
|
||||
onChatMaterialsDropdownItemClicked = onChatMaterialsDropdownItemClicked,
|
||||
onRefresh = viewModel::onRefresh,
|
||||
@@ -140,8 +147,45 @@ fun MessagesHistoryRoute(
|
||||
onActionButtonClicked = viewModel::onActionButtonClicked,
|
||||
onEmojiButtonLongClicked = viewModel::onEmojiButtonLongClicked,
|
||||
onMessageClicked = viewModel::onMessageClicked,
|
||||
onMessageLongClicked = viewModel::onMessageLongClicked
|
||||
onMessageLongClicked = viewModel::onMessageLongClicked,
|
||||
onPinnedMessageClicked = viewModel::onPinnedMessageClicked,
|
||||
onUnpinMessageButtonClicked = viewModel::onUnpinMessageClicked
|
||||
)
|
||||
|
||||
if (showMessageOptions != null) {
|
||||
val message = showMessageOptions!!
|
||||
|
||||
val messageOptions = mutableListOf(
|
||||
stringResource(UiR.string.message_context_action_reply),
|
||||
stringResource(UiR.string.message_context_action_forward)
|
||||
)
|
||||
|
||||
if (message.isPeerChat() && screenState.conversation.canChangePin) {
|
||||
messageOptions += stringResource(
|
||||
if (message.isPinned) UiR.string.message_context_action_unpin
|
||||
else UiR.string.message_context_action_pin
|
||||
)
|
||||
}
|
||||
|
||||
messageOptions += stringResource(UiR.string.message_context_action_copy)
|
||||
messageOptions += stringResource(
|
||||
if (message.isImportant) UiR.string.message_context_action_unmark_as_important
|
||||
else UiR.string.message_context_action_mark_as_important
|
||||
)
|
||||
|
||||
// if (!message.isOut) {
|
||||
// messageOptions += "Mark as spam"
|
||||
// }
|
||||
|
||||
messageOptions += stringResource(UiR.string.message_context_action_delete)
|
||||
|
||||
MaterialDialog(
|
||||
onDismissRequest = viewModel::onMessageOptionsDialogDismissed,
|
||||
selectionType = SelectionType.None,
|
||||
items = ImmutableList.copyOf(messageOptions),
|
||||
confirmText = stringResource(UiR.string.ok)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(
|
||||
@@ -152,12 +196,14 @@ fun MessagesHistoryRoute(
|
||||
@Composable
|
||||
fun MessagesHistoryScreen(
|
||||
screenState: MessagesHistoryScreenState = MessagesHistoryScreenState.EMPTY,
|
||||
scrollIndex: Int? = null,
|
||||
selectedMessages: ImmutableList<Int> = ImmutableList.empty(),
|
||||
baseError: BaseError? = null,
|
||||
canPaginate: Boolean = false,
|
||||
showEmojiButton: Boolean = false,
|
||||
onBack: () -> Unit = {},
|
||||
onClose: () -> Unit = {},
|
||||
onScrolledToIndex: () -> Unit = {},
|
||||
onSessionExpiredLogOutButtonClicked: () -> Unit = {},
|
||||
onChatMaterialsDropdownItemClicked: (peerId: Int, conversationMessageId: Int) -> Unit = { _, _ -> },
|
||||
onRefresh: () -> Unit = {},
|
||||
@@ -167,7 +213,9 @@ fun MessagesHistoryScreen(
|
||||
onActionButtonClicked: () -> Unit = {},
|
||||
onEmojiButtonLongClicked: () -> Unit = {},
|
||||
onMessageClicked: (Int) -> Unit = {},
|
||||
onMessageLongClicked: (Int) -> Unit = {}
|
||||
onMessageLongClicked: (Int) -> Unit = {},
|
||||
onPinnedMessageClicked: (Int) -> Unit = {},
|
||||
onUnpinMessageButtonClicked: () -> Unit = {}
|
||||
) {
|
||||
val view = LocalView.current
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
@@ -175,6 +223,15 @@ fun MessagesHistoryScreen(
|
||||
val listState = rememberLazyListState()
|
||||
val hazeState = remember { HazeState() }
|
||||
|
||||
LaunchedEffect(scrollIndex) {
|
||||
if (scrollIndex != null) {
|
||||
coroutineScope.launch {
|
||||
listState.animateScrollToItem(scrollIndex)
|
||||
onScrolledToIndex()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BackHandler(
|
||||
enabled = selectedMessages.isNotEmpty(),
|
||||
onBack = onClose
|
||||
@@ -440,14 +497,14 @@ fun MessagesHistoryScreen(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(56.dp)
|
||||
.clickable {
|
||||
|
||||
}
|
||||
.clickable { onPinnedMessageClicked(pinnedMessage!!.id) }
|
||||
.padding(horizontal = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.rotate(45f),
|
||||
modifier = Modifier
|
||||
.rotate(45f)
|
||||
.alpha(0.5f),
|
||||
painter = painterResource(UiR.drawable.ic_round_push_pin_24),
|
||||
contentDescription = null
|
||||
)
|
||||
@@ -468,6 +525,18 @@ fun MessagesHistoryScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (screenState.conversation.canChangePin) {
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
|
||||
IconButton(onClick = onUnpinMessageButtonClicked) {
|
||||
Icon(
|
||||
modifier = Modifier.alpha(0.5f),
|
||||
imageVector = Icons.Rounded.Close,
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
HorizontalDivider()
|
||||
}
|
||||
|
||||
+12
@@ -92,6 +92,12 @@ fun MessagesList(
|
||||
when (item) {
|
||||
is UiItem.ActionMessage -> {
|
||||
ActionMessageItem(
|
||||
modifier = Modifier.then(
|
||||
if (enableAnimations) Modifier.animateItem(
|
||||
fadeInSpec = null,
|
||||
fadeOutSpec = null
|
||||
) else Modifier
|
||||
),
|
||||
item = item,
|
||||
onClick = {
|
||||
if (item.actionCmId != null) {
|
||||
@@ -112,6 +118,12 @@ fun MessagesList(
|
||||
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.then(
|
||||
if (enableAnimations) Modifier.animateItem(
|
||||
fadeInSpec = null,
|
||||
fadeOutSpec = null
|
||||
) else Modifier
|
||||
)
|
||||
.combinedClickable(
|
||||
onLongClick = {
|
||||
if (AppSettings.General.enableHaptic) {
|
||||
|
||||
+5
-3
@@ -1,5 +1,6 @@
|
||||
package dev.meloda.fast.messageshistory.presentation
|
||||
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
@@ -19,14 +20,14 @@ fun OutgoingMessageBubble(
|
||||
animate: Boolean
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
modifier = modifier.fillMaxWidth().then(if (animate) Modifier.animateContentSize() else Modifier),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.End
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(end = 16.dp)
|
||||
.fillMaxWidth(0.75f),
|
||||
.fillMaxWidth(0.85f),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.End,
|
||||
) {
|
||||
@@ -38,7 +39,8 @@ fun OutgoingMessageBubble(
|
||||
edited = message.isEdited,
|
||||
animate = animate,
|
||||
isRead = message.isRead,
|
||||
sendingStatus = message.sendingStatus
|
||||
sendingStatus = message.sendingStatus,
|
||||
pinned = message.isPinned
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+3
-1
@@ -128,7 +128,9 @@ fun VkMessage.asPresentation(
|
||||
sendingStatus = when {
|
||||
id <= 0 -> SendingStatus.SENDING
|
||||
else -> SendingStatus.SENT
|
||||
}
|
||||
},
|
||||
isSelected = false,
|
||||
isPinned = isPinned
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user