a shit ton features, improvements and fixes in messages history screen and others

This commit is contained in:
2025-03-29 02:51:49 +03:00
parent da9644cde1
commit f02822a011
35 changed files with 1341 additions and 476 deletions
@@ -286,7 +286,7 @@ fun ConversationsScreen(
}
) {
Icon(
painter = painterResource(id = UiR.drawable.ic_baseline_create_24),
painter = painterResource(id = UiR.drawable.round_create_24),
contentDescription = "Add chat button"
)
}
@@ -0,0 +1,21 @@
package dev.meloda.fast.messageshistory.model
import dev.meloda.fast.model.api.domain.VkMessage
sealed class MessageDialog {
data class MessageOptions(val message: VkMessage) : MessageDialog()
data class MessagePin(val messageId: Int) : MessageDialog()
data class MessageUnpin(val messageId: Int) : MessageDialog()
data class MessageDelete(val message: VkMessage) : MessageDialog()
data class MessagesDelete(val messages: List<VkMessage>) : MessageDialog()
data class MessageSpam(
val message: VkMessage,
val isSpam: Boolean
) : MessageDialog()
data class MessageMarkImportance(
val message: VkMessage,
val isImportant: Boolean
) : MessageDialog()
}
@@ -0,0 +1,84 @@
package dev.meloda.fast.messageshistory.model
import android.os.Parcelable
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import dev.meloda.fast.ui.R
import kotlinx.parcelize.Parcelize
@Parcelize
sealed class MessageOption(
@StringRes val titleResId: Int,
@DrawableRes val iconResId: Int
) : Parcelable {
data object Retry : MessageOption(
titleResId = R.string.message_context_action_retry,
iconResId = R.drawable.round_restart_alt_24
)
data object Reply : MessageOption(
titleResId = R.string.message_context_action_reply,
iconResId = R.drawable.round_reply_24
)
data object ForwardHere : MessageOption(
titleResId = R.string.message_context_action_forward_here,
iconResId = R.drawable.round_reply_all_24
)
data object Forward : MessageOption(
titleResId = R.string.message_context_action_forward,
iconResId = R.drawable.round_forward_24
)
data object Pin : MessageOption(
titleResId = R.string.message_context_action_pin,
iconResId = R.drawable.pin_outline_24
)
data object Unpin : MessageOption(
titleResId = R.string.message_context_action_unpin,
iconResId = R.drawable.pin_off_outline_24
)
data object Read : MessageOption(
titleResId = R.string.message_context_action_read,
iconResId = R.drawable.round_mark_email_read_24
)
data object Copy : MessageOption(
titleResId = R.string.message_context_action_copy,
iconResId = R.drawable.round_content_copy_24
)
data object MarkAsImportant : MessageOption(
titleResId = R.string.message_context_action_mark_as_important,
iconResId = R.drawable.round_star_24
)
data object UnmarkAsImportant : MessageOption(
titleResId = R.string.message_context_action_unmark_as_important,
iconResId = R.drawable.round_star_outline_24
)
data object MarkAsSpam : MessageOption(
titleResId = R.string.message_context_action_mark_as_spam,
iconResId = R.drawable.round_report_gmailerrorred_24
)
data object UnmarkAsSpam : MessageOption(
titleResId = R.string.message_context_action_unmark_as_spam,
iconResId = R.drawable.round_report_off_24
)
data object Edit : MessageOption(
titleResId = R.string.message_context_action_edit,
iconResId = R.drawable.round_create_24
)
data object Delete : MessageOption(
titleResId = R.string.message_context_action_delete,
iconResId = R.drawable.round_delete_outline_24
)
}
@@ -14,7 +14,6 @@ data class MessagesHistoryScreenState(
val title: String,
val status: String?,
val avatar: UiImage,
val messages: List<UiItem>,
val message: TextFieldValue,
val attachments: List<VkAttachment>,
val isLoading: Boolean,
@@ -34,7 +33,6 @@ data class MessagesHistoryScreenState(
title = "",
status = null,
avatar = UiImage.Color(0),
messages = emptyList(),
message = TextFieldValue(),
attachments = emptyList(),
isLoading = true,
@@ -26,7 +26,8 @@ sealed class UiItem(
val isRead: Boolean,
val sendingStatus: SendingStatus,
val isSelected: Boolean,
val isPinned: Boolean
val isPinned: Boolean,
val isImportant: Boolean
) : UiItem(id, conversationMessageId)
data class ActionMessage(
@@ -90,7 +90,9 @@ fun IncomingMessageBubble(
edited = message.isEdited,
isRead = message.isRead,
sendingStatus = message.sendingStatus,
pinned = message.isPinned
pinned = message.isPinned,
important = message.isImportant,
isSelected = message.isSelected
)
}
}
@@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Create
import androidx.compose.material3.Icon
@@ -20,6 +21,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
@@ -42,7 +44,9 @@ fun MessageBubble(
edited: Boolean,
isRead: Boolean,
sendingStatus: SendingStatus,
pinned: Boolean
pinned: Boolean,
important: Boolean,
isSelected: Boolean
) {
val theme = LocalThemeConfig.current
val backgroundColor = if (!isOut) {
@@ -68,12 +72,15 @@ fun MessageBubble(
)
.then(if (theme.enableAnimations) 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
val minDateContainerWidth by remember(edited, isOut, pinned, important) {
derivedStateOf {
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
val importantIndicatorPart = if (important) 14.dp else 0.dp
mainPart + readIndicatorPart + pinnedIndicatorPart
mainPart + readIndicatorPart + pinnedIndicatorPart + importantIndicatorPart
}
}
val dateContainerWidth by animateDpAsState(
@@ -82,17 +89,29 @@ fun MessageBubble(
)
if (text != null) {
Text(
text = text,
modifier = Modifier
.padding(2.dp)
.align(Alignment.Center)
.padding(end = 4.dp)
.padding(end = dateContainerWidth)
.padding(end = 4.dp)
.then(if (theme.enableAnimations) Modifier.animateContentSize() else Modifier),
color = textColor
)
val textLambda: @Composable () -> Unit = remember {
{
Text(
text = text,
modifier = Modifier
.padding(2.dp)
.align(Alignment.Center)
.padding(end = 4.dp)
.padding(end = dateContainerWidth)
.padding(end = 4.dp)
.then(if (theme.enableAnimations) Modifier.animateContentSize() else Modifier),
color = textColor
)
}
}
if (isSelected) {
SelectionContainer {
textLambda.invoke()
}
} else {
textLambda.invoke()
}
}
Row(
@@ -101,6 +120,14 @@ fun MessageBubble(
.defaultMinSize(minWidth = dateContainerWidth)
.then(if (theme.enableAnimations) Modifier.animateContentSize() else Modifier),
) {
if (important) {
Icon(
painter = painterResource(UiR.drawable.round_star_24),
contentDescription = null,
modifier = Modifier.size(14.dp)
)
Spacer(modifier = Modifier.width(4.dp))
}
if (pinned) {
Icon(
painter = painterResource(UiR.drawable.ic_round_push_pin_24),
@@ -119,6 +146,7 @@ fun MessageBubble(
)
Spacer(modifier = Modifier.width(4.dp))
}
Text(
text = date.orEmpty(),
style = MaterialTheme.typography.labelSmall,
@@ -1,5 +1,6 @@
package dev.meloda.fast.messageshistory.presentation
import android.os.Bundle
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateColorAsState
@@ -39,6 +40,7 @@ import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material.icons.outlined.MoreVert
import androidx.compose.material.icons.rounded.Close
import androidx.compose.material.icons.rounded.Refresh
import androidx.compose.material3.Checkbox
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
@@ -53,6 +55,7 @@ import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.minimumInteractiveComponentSize
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@@ -64,7 +67,6 @@ 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
@@ -74,12 +76,12 @@ 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
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.core.os.bundleOf
import androidx.core.view.HapticFeedbackConstantsCompat
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coil.compose.AsyncImage
@@ -93,22 +95,28 @@ import dev.meloda.fast.datastore.UserSettings
import dev.meloda.fast.messageshistory.MessagesHistoryViewModel
import dev.meloda.fast.messageshistory.MessagesHistoryViewModelImpl
import dev.meloda.fast.messageshistory.model.ActionMode
import dev.meloda.fast.messageshistory.model.MessageDialog
import dev.meloda.fast.messageshistory.model.MessageOption
import dev.meloda.fast.messageshistory.model.MessagesHistoryScreenState
import dev.meloda.fast.messageshistory.model.UiItem
import dev.meloda.fast.messageshistory.util.firstMessage
import dev.meloda.fast.messageshistory.util.indexOfMessageByCmId
import dev.meloda.fast.model.BaseError
import dev.meloda.fast.model.api.domain.VkMessage
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.ImmutableList.Companion.toImmutableList
import dev.meloda.fast.ui.util.emptyImmutableList
import dev.meloda.fast.ui.util.getImage
import kotlinx.coroutines.launch
import org.koin.androidx.compose.koinViewModel
import org.koin.compose.koinInject
import java.util.concurrent.TimeUnit
import dev.meloda.fast.ui.R as UiR
@Composable
@@ -119,10 +127,12 @@ fun MessagesHistoryRoute(
viewModel: MessagesHistoryViewModel = koinViewModel<MessagesHistoryViewModelImpl>()
) {
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
val messages by viewModel.messages.collectAsStateWithLifecycle()
val uiMessages by viewModel.uiMessages.collectAsStateWithLifecycle()
val messageDialog by viewModel.messageDialog.collectAsStateWithLifecycle()
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()
@@ -130,8 +140,10 @@ fun MessagesHistoryRoute(
MessagesHistoryScreen(
screenState = screenState,
messages = messages.toImmutableList(),
uiMessages = uiMessages.toImmutableList(),
scrollIndex = scrollIndex,
selectedMessages = ImmutableList.copyOf(selectedMessages),
selectedMessages = selectedMessages.toImmutableList(),
baseError = baseError,
canPaginate = canPaginate,
showEmojiButton = showEmojiButton,
@@ -149,43 +161,300 @@ fun MessagesHistoryRoute(
onMessageClicked = viewModel::onMessageClicked,
onMessageLongClicked = viewModel::onMessageLongClicked,
onPinnedMessageClicked = viewModel::onPinnedMessageClicked,
onUnpinMessageButtonClicked = viewModel::onUnpinMessageClicked
onUnpinMessageButtonClicked = viewModel::onUnpinMessageClicked,
onDeleteSelectedButtonClicked = viewModel::onDeleteSelectedMessagesClicked
)
if (showMessageOptions != null) {
val message = showMessageOptions!!
HandleDialogs(
screenState = screenState,
messageDialog = messageDialog,
onConfirmed = viewModel::onDialogConfirmed,
onCancelled = viewModel::onDialogCancelled,
onDismissed = viewModel::onDialogDismissed,
onItemPicked = viewModel::onDialogItemPicked
)
}
val messageOptions = mutableListOf(
stringResource(UiR.string.message_context_action_reply),
stringResource(UiR.string.message_context_action_forward)
)
@Composable
fun HandleDialogs(
screenState: MessagesHistoryScreenState,
messageDialog: MessageDialog?,
onConfirmed: (MessageDialog, Bundle) -> Unit = { _, _ -> },
onCancelled: (MessageDialog) -> Unit = {},
onDismissed: (MessageDialog) -> Unit = {},
onItemPicked: (MessageDialog, Bundle) -> Unit = { _, _ -> }
) {
when (messageDialog) {
null -> Unit
if (message.isPeerChat() && screenState.conversation.canChangePin) {
messageOptions += stringResource(
if (message.isPinned) UiR.string.message_context_action_unpin
else UiR.string.message_context_action_pin
is MessageDialog.MessageOptions -> {
MessageOptionsDialog(
screenState = screenState,
message = messageDialog.message,
onDismissed = { onDismissed(messageDialog) },
onItemPicked = { bundle -> onItemPicked(messageDialog, bundle) }
)
}
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
)
is MessageDialog.MessageDelete -> {
MessageDeleteDialog(
messages = listOf(messageDialog.message),
onConfirmed = { onConfirmed(messageDialog, it) },
onDismissed = { onDismissed(messageDialog) }
)
}
// if (!message.isOut) {
// messageOptions += "Mark as spam"
// }
is MessageDialog.MessagesDelete -> {
MessageDeleteDialog(
messages = messageDialog.messages,
onConfirmed = { onConfirmed(messageDialog, it) },
onDismissed = { onDismissed(messageDialog) }
)
}
messageOptions += stringResource(UiR.string.message_context_action_delete)
is MessageDialog.MessagePin,
is MessageDialog.MessageUnpin -> {
MessagePinStateDialog(
pin = messageDialog is MessageDialog.MessagePin,
onConfirmed = { onConfirmed(messageDialog, bundleOf()) },
onDismissed = { onDismissed(messageDialog) }
)
}
MaterialDialog(
onDismissRequest = viewModel::onMessageOptionsDialogDismissed,
selectionType = SelectionType.None,
items = ImmutableList.copyOf(messageOptions),
confirmText = stringResource(UiR.string.ok)
is MessageDialog.MessageMarkImportance -> {
MessageImportanceDialog(
important = messageDialog.isImportant,
onConfirmed = { onConfirmed(messageDialog, bundleOf()) },
onDismissed = { onDismissed(messageDialog) }
)
}
is MessageDialog.MessageSpam -> {
MessageSpamDialog(
spam = messageDialog.isSpam,
onConfirmed = { onConfirmed(messageDialog, bundleOf()) },
onDismissed = { onDismissed(messageDialog) }
)
}
}
}
@Composable
fun MessageOptionsDialog(
screenState: MessagesHistoryScreenState,
message: VkMessage,
onDismissed: () -> Unit = {},
onItemPicked: (Bundle) -> Unit
) {
val options = mutableListOf<MessageOption>()
if (message.isFailed()) {
options += MessageOption.Retry
} else {
options += MessageOption.Reply
options += MessageOption.ForwardHere
options += MessageOption.Forward
if (message.isPeerChat() && screenState.conversation.canChangePin) {
options += if (message.isPinned) MessageOption.Unpin else MessageOption.Pin
}
if (!message.isRead(screenState.conversation)) {
options += MessageOption.Read
}
options += MessageOption.Copy
if (message.isOut) {
val diff = System.currentTimeMillis() - message.date * 1000L
if (diff - TimeUnit.DAYS.toMillis(1) <= 0) {
options += MessageOption.Edit
}
}
options += if (message.isImportant) MessageOption.UnmarkAsImportant
else MessageOption.MarkAsImportant
if (!message.isOut) {
options += if (message.isSpam) MessageOption.UnmarkAsSpam
else MessageOption.MarkAsSpam
}
}
options += MessageOption.Delete
val messageOptions = options.map { option ->
Triple(
stringResource(option.titleResId),
painterResource(option.iconResId),
when {
option in listOf(
MessageOption.Delete,
MessageOption.MarkAsSpam
) -> MaterialTheme.colorScheme.error
else -> MaterialTheme.colorScheme.primary
}
)
}
MaterialDialog(onDismissRequest = onDismissed) {
messageOptions
.forEachIndexed { index, (title, painter, tintColor) ->
DropdownMenuItem(
text = {
Row {
Text(text = title)
Spacer(modifier = Modifier.width(8.dp))
}
},
leadingIcon = {
Row {
Spacer(modifier = Modifier.width(8.dp))
Icon(
painter = painter,
contentDescription = null,
tint = tintColor
)
}
},
onClick = {
onDismissed()
val pickedOption = options[index]
onItemPicked(bundleOf("option" to pickedOption))
}
)
}
}
}
@Composable
fun MessageDeleteDialog(
messages: List<VkMessage>,
onConfirmed: (Bundle) -> Unit = {},
onDismissed: () -> Unit = {},
) {
var forEveryone by remember {
mutableStateOf(messages.all(VkMessage::isOut))
}
val shouldBeDisabled by remember(messages) {
mutableStateOf(messages.any(VkMessage::isFailed) || !messages.all(VkMessage::isOut))
}
MaterialDialog(
onDismissRequest = onDismissed,
title = stringResource(UiR.string.delete_message_title),
confirmText = stringResource(UiR.string.action_delete),
confirmAction = {
onConfirmed(
bundleOf("everyone" to if (messages.all(VkMessage::isOut)) forEveryone else false)
)
},
cancelText = stringResource(UiR.string.cancel),
) {
Row(
modifier = Modifier
.then(
if (!shouldBeDisabled) {
Modifier.clickable { forEveryone = !forEveryone }
} else Modifier)
.fillMaxWidth()
.minimumInteractiveComponentSize()
.padding(start = 24.dp, end = 16.dp)
) {
Checkbox(
checked = forEveryone,
onCheckedChange = null,
enabled = !shouldBeDisabled
)
Spacer(modifier = Modifier.width(8.dp))
LocalContentAlpha(
alpha = if (shouldBeDisabled) ContentAlpha.disabled
else ContentAlpha.high
) {
Text(text = stringResource(UiR.string.delete_message_for_everyone))
}
}
}
}
@Composable
fun MessagePinStateDialog(
pin: Boolean,
onConfirmed: () -> Unit = {},
onDismissed: () -> Unit = {},
) {
MaterialDialog(
onDismissRequest = onDismissed,
title = stringResource(
if (pin) UiR.string.pin_message_title
else UiR.string.unpin_message_title
),
text = stringResource(
if (pin) UiR.string.pin_message_text
else UiR.string.unpin_message_text
),
confirmText = stringResource(
if (pin) UiR.string.action_pin
else UiR.string.action_unpin
),
confirmAction = onConfirmed,
cancelText = stringResource(UiR.string.cancel)
)
}
@Composable
fun MessageImportanceDialog(
important: Boolean,
onConfirmed: () -> Unit = {},
onDismissed: () -> Unit = {},
) {
MaterialDialog(
onDismissRequest = onDismissed,
title = stringResource(
if (important) UiR.string.important_message_title
else UiR.string.unimportant_message_title
),
text = stringResource(
if (important) UiR.string.important_message_text
else UiR.string.unimportant_message_text
),
confirmText = stringResource(
if (important) UiR.string.action_mark
else UiR.string.action_unmark
),
confirmAction = onConfirmed,
cancelText = stringResource(UiR.string.cancel)
)
}
@Composable
fun MessageSpamDialog(
spam: Boolean,
onConfirmed: () -> Unit = {},
onDismissed: () -> Unit = {},
) {
MaterialDialog(
onDismissRequest = onDismissed,
title = stringResource(
if (spam) UiR.string.spam_message_title
else UiR.string.unspam_message_title
),
text = stringResource(
if (spam) UiR.string.spam_message_text
else UiR.string.unspam_message_text
),
confirmText = stringResource(
if (spam) UiR.string.action_mark
else UiR.string.action_unmark
),
confirmAction = onConfirmed,
cancelText = stringResource(UiR.string.cancel)
)
}
@OptIn(
@@ -196,8 +465,10 @@ fun MessagesHistoryRoute(
@Composable
fun MessagesHistoryScreen(
screenState: MessagesHistoryScreenState = MessagesHistoryScreenState.EMPTY,
messages: ImmutableList<VkMessage> = emptyImmutableList(),
uiMessages: ImmutableList<UiItem> = emptyImmutableList(),
scrollIndex: Int? = null,
selectedMessages: ImmutableList<Int> = ImmutableList.empty(),
selectedMessages: ImmutableList<VkMessage> = emptyImmutableList(),
baseError: BaseError? = null,
canPaginate: Boolean = false,
showEmojiButton: Boolean = false,
@@ -215,7 +486,8 @@ fun MessagesHistoryScreen(
onMessageClicked: (Int) -> Unit = {},
onMessageLongClicked: (Int) -> Unit = {},
onPinnedMessageClicked: (Int) -> Unit = {},
onUnpinMessageButtonClicked: () -> Unit = {}
onUnpinMessageButtonClicked: () -> Unit = {},
onDeleteSelectedButtonClicked: () -> Unit = {}
) {
val view = LocalView.current
val coroutineScope = rememberCoroutineScope()
@@ -288,8 +560,8 @@ fun MessagesHistoryScreen(
val density = LocalDensity.current
val showReplyAction by remember(screenState) {
mutableStateOf(selectedMessages.size == 1)
val showReplyAction by remember(selectedMessages) {
derivedStateOf { selectedMessages.size == 1 }
}
Scaffold(
@@ -414,6 +686,12 @@ fun MessagesHistoryScreen(
}
}
) {
Icon(
painter = painterResource(UiR.drawable.round_forward_24),
contentDescription = null
)
}
IconButton(onClick = onDeleteSelectedButtonClicked) {
Icon(
painter = painterResource(UiR.drawable.round_delete_outline_24),
contentDescription = null
@@ -449,7 +727,7 @@ fun MessagesHistoryScreen(
// TODO: 23-Mar-25, Danil Nikolaev: crash if no messages (ex. new chat)
onChatMaterialsDropdownItemClicked(
screenState.conversationId,
screenState.messages.firstMessage().conversationMessageId
uiMessages.values.firstMessage().conversationMessageId
)
},
text = {
@@ -483,7 +761,7 @@ fun MessagesHistoryScreen(
)
val showHorizontalProgressBar by remember(screenState) {
derivedStateOf { screenState.isLoading && screenState.messages.isNotEmpty() }
derivedStateOf { screenState.isLoading && messages.isNotEmpty() }
}
if (showHorizontalProgressBar) {
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
@@ -493,51 +771,15 @@ fun MessagesHistoryScreen(
}
if (!screenState.isLoading && pinnedMessage != null) {
Row(
modifier = Modifier
.fillMaxWidth()
.height(56.dp)
.clickable { onPinnedMessageClicked(pinnedMessage!!.id) }
.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
modifier = Modifier
.rotate(45f)
.alpha(0.5f),
painter = painterResource(UiR.drawable.ic_round_push_pin_24),
contentDescription = null
)
Spacer(modifier = Modifier.width(16.dp))
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = screenState.pinnedTitle.orDots(),
fontWeight = FontWeight.Medium,
color = MaterialTheme.colorScheme.primary
)
screenState.pinnedSummary?.let { summary ->
LocalContentAlpha(alpha = ContentAlpha.medium) {
Text(text = summary)
}
}
}
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
)
}
}
}
PinnedMessageContainer(
modifier = Modifier,
pinnedMessage = requireNotNull(pinnedMessage),
title = screenState.pinnedTitle.orDots(),
summary = screenState.pinnedSummary,
canChangePin = screenState.conversation.canChangePin,
onPinnedMessageClicked = onPinnedMessageClicked,
onUnpinMessageButtonClicked = onUnpinMessageButtonClicked
)
HorizontalDivider()
}
}
@@ -551,16 +793,17 @@ fun MessagesHistoryScreen(
.padding(bottom = padding.calculateBottomPadding()),
) {
MessagesList(
modifier = Modifier.align(Alignment.BottomStart),
hazeState = hazeState,
listState = listState,
hasPinnedMessage = pinnedMessage != null,
immutableMessages = ImmutableList.copyOf(screenState.messages),
uiMessages = uiMessages,
isPaginating = screenState.isPaginating,
messageBarHeight = messageBarHeight,
onRequestScrollToCmId = { cmId ->
coroutineScope.launch {
listState.animateScrollToItem(
index = screenState.messages.indexOfMessageByCmId(cmId)
index = uiMessages.values.indexOfMessageByCmId(cmId)
)
}
},
@@ -775,7 +1018,7 @@ fun MessagesHistoryScreen(
}
when {
screenState.isLoading && screenState.messages.isEmpty() -> {
screenState.isLoading && messages.values.isEmpty() -> {
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
}
@@ -42,15 +42,15 @@ fun MessagesList(
hasPinnedMessage: Boolean,
hazeState: HazeState,
listState: LazyListState,
immutableMessages: ImmutableList<UiItem>,
uiMessages: ImmutableList<UiItem>,
isPaginating: Boolean,
messageBarHeight: Dp,
onRequestScrollToCmId: (cmId: Int) -> Unit = {},
onMessageClicked: (Int) -> Unit = {},
onMessageLongClicked: (Int) -> Unit = {}
) {
val messages = remember(immutableMessages) {
immutableMessages.toList()
val messages = remember(uiMessages) {
uiMessages.toList()
}
val theme = LocalThemeConfig.current
val view = LocalView.current
@@ -44,7 +44,9 @@ fun OutgoingMessageBubble(
edited = message.isEdited,
isRead = message.isRead,
sendingStatus = message.sendingStatus,
pinned = message.isPinned
pinned = message.isPinned,
important = message.isImportant,
isSelected = message.isSelected
)
}
}
@@ -0,0 +1,88 @@
package dev.meloda.fast.messageshistory.presentation
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Close
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import dev.meloda.fast.model.api.domain.VkMessage
import dev.meloda.fast.ui.R
import dev.meloda.fast.ui.basic.ContentAlpha
import dev.meloda.fast.ui.basic.LocalContentAlpha
import dev.meloda.fast.ui.components.IconButton
@Composable
fun PinnedMessageContainer(
modifier: Modifier = Modifier,
pinnedMessage: VkMessage,
title: String,
summary: AnnotatedString?,
canChangePin: Boolean,
onPinnedMessageClicked: (Int) -> Unit = {},
onUnpinMessageButtonClicked: () -> Unit = {}
) {
Row(
modifier = modifier
.fillMaxWidth()
.height(56.dp)
.clickable { onPinnedMessageClicked(pinnedMessage.id) }
.padding(start = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
modifier = Modifier
.rotate(45f)
.alpha(0.5f),
painter = painterResource(R.drawable.ic_round_push_pin_24),
contentDescription = null
)
Spacer(modifier = Modifier.width(16.dp))
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = title,
fontWeight = FontWeight.Medium,
color = MaterialTheme.colorScheme.primary
)
summary?.let { summary ->
LocalContentAlpha(alpha = ContentAlpha.medium) {
Text(text = summary)
}
}
}
if (canChangePin) {
Spacer(modifier = Modifier.width(16.dp))
IconButton(onClick = onUnpinMessageButtonClicked) {
Icon(
modifier = Modifier.alpha(0.5f),
imageVector = Icons.Rounded.Close,
contentDescription = null
)
}
Spacer(modifier = Modifier.width(8.dp))
}
}
}
@@ -96,7 +96,8 @@ fun VkMessage.asPresentation(
showName: Boolean,
prevMessage: VkMessage?,
nextMessage: VkMessage?,
showTimeInActionMessages: Boolean
showTimeInActionMessages: Boolean,
isSelected: Boolean
): UiItem = when {
action != null -> UiItem.ActionMessage(
id = id,
@@ -126,11 +127,13 @@ fun VkMessage.asPresentation(
isEdited = updateTime != null,
isRead = isRead(conversation),
sendingStatus = when {
isFailed() -> SendingStatus.FAILED
id <= 0 -> SendingStatus.SENDING
else -> SendingStatus.SENT
},
isSelected = false,
isPinned = isPinned
isSelected = isSelected,
isPinned = isPinned,
isImportant = isImportant
)
}
@@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
@@ -139,8 +140,11 @@ fun EditTextAlert(
cancelText = stringResource(id = R.string.cancel),
actionInvokeDismiss = ActionInvokeDismiss.Always
) {
Row(modifier = Modifier.fillMaxWidth()) {
Spacer(modifier = Modifier.width(20.dp))
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp)
) {
TextField(
modifier = Modifier
.fillMaxWidth()
@@ -155,8 +159,8 @@ fun EditTextAlert(
placeholder = { Text(text = "Value") },
shape = RoundedCornerShape(10.dp),
)
Spacer(modifier = Modifier.width(20.dp))
}
Spacer(modifier = Modifier.height(8.dp))
}
LaunchedEffect(Unit) {