forked from melod1n/fast-messenger
Refactor: Implement swipe-to-reply and redesign input bar
This commit introduces the ability to reply to a message by swiping it to the right. The message input bar and related components have been redesigned and refactored for a cleaner look and better user experience. Key changes: - Added a swipe-to-reply gesture on message bubbles. - Redesigned the message `InputBar` with updated styling, animations, and rounded corners that adapt to the reply state. - Renamed `MessagesHistoryInputBar` to a more generic `InputBar`. - Introduced `FastTextField`, a customized `BasicTextField`, for better performance and control. - Replaced `IconButton` with `FastIconButton` and `RippledClickContainer` in several places for consistent click handling. - Refactored `PinnedMessageContainer` and `ReplyContainer` with improved UI. - Updated the Compose BOM to `2025.12.00`.
This commit is contained in:
+1
-1
@@ -24,7 +24,7 @@ import androidx.compose.ui.unit.dp
|
|||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3ExpressiveApi::class)
|
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3ExpressiveApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun IconButton(
|
fun FastIconButton(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onClick: () -> Unit = {},
|
onClick: () -> Unit = {},
|
||||||
onLongClick: (() -> Unit)? = null,
|
onLongClick: (() -> Unit)? = null,
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
package dev.meloda.fast.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.foundation.text.selection.LocalTextSelectionColors
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.LocalTextStyle
|
||||||
|
import androidx.compose.material3.TextFieldColors
|
||||||
|
import androidx.compose.material3.TextFieldDefaults
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Shape
|
||||||
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
|
import androidx.compose.ui.graphics.takeOrElse
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
|
import androidx.compose.ui.text.input.VisualTransformation
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun FastTextField(
|
||||||
|
value: TextFieldValue,
|
||||||
|
onValueChange: (TextFieldValue) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
enabled: Boolean = true,
|
||||||
|
readOnly: Boolean = false,
|
||||||
|
textStyle: TextStyle = LocalTextStyle.current,
|
||||||
|
label: @Composable (() -> Unit)? = null,
|
||||||
|
placeholder: @Composable (() -> Unit)? = null,
|
||||||
|
leadingIcon: @Composable (() -> Unit)? = null,
|
||||||
|
trailingIcon: @Composable (() -> Unit)? = null,
|
||||||
|
prefix: @Composable (() -> Unit)? = null,
|
||||||
|
suffix: @Composable (() -> Unit)? = null,
|
||||||
|
supportingText: @Composable (() -> Unit)? = null,
|
||||||
|
isError: Boolean = false,
|
||||||
|
visualTransformation: VisualTransformation = VisualTransformation.None,
|
||||||
|
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||||
|
keyboardActions: KeyboardActions = KeyboardActions.Default,
|
||||||
|
singleLine: Boolean = false,
|
||||||
|
maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
|
||||||
|
minLines: Int = 1,
|
||||||
|
interactionSource: MutableInteractionSource? = null,
|
||||||
|
shape: Shape = TextFieldDefaults.shape,
|
||||||
|
colors: TextFieldColors = TextFieldDefaults.colors(),
|
||||||
|
) {
|
||||||
|
@Suppress("NAME_SHADOWING")
|
||||||
|
val interactionSource = interactionSource ?: remember { MutableInteractionSource() }
|
||||||
|
// If color is not provided via the text style, use content color as a default
|
||||||
|
val textColor =
|
||||||
|
textStyle.color.takeOrElse {
|
||||||
|
val focused = interactionSource.collectIsFocusedAsState().value
|
||||||
|
colors.textColor(enabled, isError, focused)
|
||||||
|
}
|
||||||
|
val mergedTextStyle = textStyle.merge(TextStyle(color = textColor))
|
||||||
|
|
||||||
|
CompositionLocalProvider(LocalTextSelectionColors provides colors.textSelectionColors) {
|
||||||
|
BasicTextField(
|
||||||
|
value = value,
|
||||||
|
modifier =
|
||||||
|
modifier,
|
||||||
|
/* .defaultMinSize(
|
||||||
|
minWidth = TextFieldDefaults.MinWidth,
|
||||||
|
minHeight = TextFieldDefaults.MinHeight,
|
||||||
|
)*/
|
||||||
|
onValueChange = onValueChange,
|
||||||
|
enabled = enabled,
|
||||||
|
readOnly = readOnly,
|
||||||
|
textStyle = mergedTextStyle,
|
||||||
|
cursorBrush = SolidColor(colors.cursorColor(isError)),
|
||||||
|
visualTransformation = visualTransformation,
|
||||||
|
keyboardOptions = keyboardOptions,
|
||||||
|
keyboardActions = keyboardActions,
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
singleLine = singleLine,
|
||||||
|
maxLines = maxLines,
|
||||||
|
minLines = minLines,
|
||||||
|
decorationBox =
|
||||||
|
@Composable { innerTextField ->
|
||||||
|
// places leading icon, text field with label and placeholder, trailing icon
|
||||||
|
TextFieldDefaults.DecorationBox(
|
||||||
|
value = value.text,
|
||||||
|
visualTransformation = visualTransformation,
|
||||||
|
innerTextField = innerTextField,
|
||||||
|
placeholder = placeholder,
|
||||||
|
label = label,
|
||||||
|
leadingIcon = leadingIcon,
|
||||||
|
trailingIcon = trailingIcon,
|
||||||
|
prefix = prefix,
|
||||||
|
suffix = suffix,
|
||||||
|
supportingText = supportingText,
|
||||||
|
shape = shape,
|
||||||
|
singleLine = singleLine,
|
||||||
|
enabled = enabled,
|
||||||
|
isError = isError,
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
colors = colors,
|
||||||
|
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 12.dp)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
package dev.meloda.fast.ui.components
|
package dev.meloda.fast.ui.components
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.ripple
|
import androidx.compose.material3.ripple
|
||||||
@@ -16,15 +16,17 @@ fun RippledClickContainer(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
shape: Shape = RoundedCornerShape(4.dp),
|
shape: Shape = RoundedCornerShape(4.dp),
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
|
onLongClick: (() -> Unit)? = null,
|
||||||
content: @Composable () -> Unit
|
content: @Composable () -> Unit
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.clip(shape)
|
.clip(shape)
|
||||||
.clickable(
|
.combinedClickable(
|
||||||
interactionSource = null,
|
interactionSource = null,
|
||||||
indication = ripple(),
|
indication = ripple(),
|
||||||
onClick = onClick
|
onClick = onClick,
|
||||||
|
onLongClick = onLongClick
|
||||||
),
|
),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
|
|||||||
+2
-2
@@ -63,7 +63,7 @@ import dev.meloda.fast.conversations.model.CreateChatScreenState
|
|||||||
import dev.meloda.fast.model.BaseError
|
import dev.meloda.fast.model.BaseError
|
||||||
import dev.meloda.fast.ui.R
|
import dev.meloda.fast.ui.R
|
||||||
import dev.meloda.fast.ui.components.FullScreenContainedLoader
|
import dev.meloda.fast.ui.components.FullScreenContainedLoader
|
||||||
import dev.meloda.fast.ui.components.IconButton
|
import dev.meloda.fast.ui.components.FastIconButton
|
||||||
import dev.meloda.fast.ui.components.MaterialDialog
|
import dev.meloda.fast.ui.components.MaterialDialog
|
||||||
import dev.meloda.fast.ui.components.NoItemsView
|
import dev.meloda.fast.ui.components.NoItemsView
|
||||||
import dev.meloda.fast.ui.components.VkErrorView
|
import dev.meloda.fast.ui.components.VkErrorView
|
||||||
@@ -205,7 +205,7 @@ fun CreateChatScreen(
|
|||||||
) {
|
) {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = onBack) {
|
FastIconButton(onClick = onBack) {
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(R.drawable.round_arrow_back_24px),
|
painter = painterResource(R.drawable.round_arrow_back_24px),
|
||||||
contentDescription = null
|
contentDescription = null
|
||||||
|
|||||||
+2
@@ -63,4 +63,6 @@ interface MessagesHistoryViewModel {
|
|||||||
fun onRegularClicked()
|
fun onRegularClicked()
|
||||||
|
|
||||||
fun onReplyCloseClicked()
|
fun onReplyCloseClicked()
|
||||||
|
|
||||||
|
fun onRequestReplyToMessage(cmId: Long)
|
||||||
}
|
}
|
||||||
|
|||||||
+22
-23
@@ -61,7 +61,6 @@ import dev.meloda.fast.network.VkErrorCode
|
|||||||
import dev.meloda.fast.ui.R
|
import dev.meloda.fast.ui.R
|
||||||
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.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
@@ -235,22 +234,7 @@ class MessagesHistoryViewModelImpl(
|
|||||||
// TODO: 28-Mar-25, Danil Nikolaev: retry sending
|
// TODO: 28-Mar-25, Danil Nikolaev: retry sending
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageOption.Reply -> {
|
MessageOption.Reply -> replyToMessage(cmId)
|
||||||
inputFieldFocusRequester.setValue { true }
|
|
||||||
replyToCmId = cmId
|
|
||||||
screenState.setValue { old ->
|
|
||||||
val msg = messages.value.find { it.id == messageId }
|
|
||||||
|
|
||||||
if (msg == null) {
|
|
||||||
old
|
|
||||||
} else {
|
|
||||||
old.copy(
|
|
||||||
replyTitle = msg.extractTitle(),
|
|
||||||
replyText = msg.text
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MessageOption.ForwardHere -> {
|
MessageOption.ForwardHere -> {
|
||||||
|
|
||||||
@@ -349,12 +333,10 @@ class MessagesHistoryViewModelImpl(
|
|||||||
|
|
||||||
override fun onEmojiButtonLongClicked() {
|
override fun onEmojiButtonLongClicked() {
|
||||||
AppSettings.Features.fastText.takeIf { it.isNotBlank() }?.let { text ->
|
AppSettings.Features.fastText.takeIf { it.isNotBlank() }?.let { text ->
|
||||||
screenState.setValue { old ->
|
val newText = "${screenState.value.message.text}$text"
|
||||||
val newText = "${old.message.text}$text"
|
onMessageInputChanged(
|
||||||
old.copy(
|
TextFieldValue(text = newText, selection = TextRange(newText.length))
|
||||||
message = TextFieldValue(text = newText, selection = TextRange(newText.length))
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -448,6 +430,19 @@ class MessagesHistoryViewModelImpl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun replyToMessage(cmId: Long) {
|
||||||
|
val messageToReply = messages.value.find { it.cmId == cmId } ?: return
|
||||||
|
|
||||||
|
inputFieldFocusRequester.setValue { true }
|
||||||
|
replyToCmId = cmId
|
||||||
|
screenState.setValue { old ->
|
||||||
|
old.copy(
|
||||||
|
replyTitle = messageToReply.extractTitle(),
|
||||||
|
replyText = messageToReply.text
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private var formatData = VkMessage.FormatData("1", emptyList())
|
private var formatData = VkMessage.FormatData("1", emptyList())
|
||||||
|
|
||||||
private fun updateStyles() {
|
private fun updateStyles() {
|
||||||
@@ -576,6 +571,10 @@ class MessagesHistoryViewModelImpl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onRequestReplyToMessage(cmId: Long) {
|
||||||
|
replyToMessage(cmId)
|
||||||
|
}
|
||||||
|
|
||||||
private fun handleNewMessage(event: LongPollParsedEvent.NewMessage) {
|
private fun handleNewMessage(event: LongPollParsedEvent.NewMessage) {
|
||||||
val message = event.message
|
val message = event.message
|
||||||
|
|
||||||
|
|||||||
+75
-54
@@ -4,15 +4,18 @@ import androidx.compose.animation.AnimatedVisibility
|
|||||||
import androidx.compose.animation.animateContentSize
|
import androidx.compose.animation.animateContentSize
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.offset
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.layout.widthIn
|
import androidx.compose.foundation.layout.widthIn
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -25,18 +28,23 @@ import androidx.compose.ui.draw.clip
|
|||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.IntOffset
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import coil.compose.rememberAsyncImagePainter
|
import coil.compose.rememberAsyncImagePainter
|
||||||
import coil.imageLoader
|
import coil.imageLoader
|
||||||
|
import com.conena.nanokt.android.content.dpInPx
|
||||||
import dev.meloda.fast.messageshistory.model.UiItem
|
import dev.meloda.fast.messageshistory.model.UiItem
|
||||||
import dev.meloda.fast.model.api.domain.VkAttachment
|
import dev.meloda.fast.model.api.domain.VkAttachment
|
||||||
|
import dev.meloda.fast.ui.R
|
||||||
import dev.meloda.fast.ui.util.ImmutableList.Companion.toImmutableList
|
import dev.meloda.fast.ui.util.ImmutableList.Companion.toImmutableList
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun IncomingMessageBubble(
|
fun IncomingMessageBubble(
|
||||||
enableAnimations: Boolean,
|
enableAnimations: Boolean,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
message: UiItem.Message,
|
message: UiItem.Message,
|
||||||
|
offsetX: Float = 0f,
|
||||||
onClick: (VkAttachment) -> Unit = {},
|
onClick: (VkAttachment) -> Unit = {},
|
||||||
onLongClick: (VkAttachment) -> Unit = {},
|
onLongClick: (VkAttachment) -> Unit = {},
|
||||||
onReplyClick: () -> Unit = {}
|
onReplyClick: () -> Unit = {}
|
||||||
@@ -53,66 +61,79 @@ fun IncomingMessageBubble(
|
|||||||
else Modifier
|
else Modifier
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
Row(
|
Box(modifier = Modifier.fillMaxWidth()) {
|
||||||
modifier = Modifier
|
Icon(
|
||||||
.fillMaxWidth(0.85f)
|
painter = painterResource(R.drawable.round_reply_24),
|
||||||
.padding(start = 16.dp),
|
contentDescription = null,
|
||||||
verticalAlignment = Alignment.Bottom,
|
tint = MaterialTheme.colorScheme.secondary,
|
||||||
horizontalArrangement = Arrangement.Start
|
modifier = Modifier
|
||||||
) {
|
.align(Alignment.CenterEnd)
|
||||||
if (message.isInChat) {
|
.offset { IntOffset(24.dpInPx + offsetX.roundToInt(), y = 0) }
|
||||||
Image(
|
)
|
||||||
painter =
|
|
||||||
message.avatar.extractUrl()?.let { url ->
|
|
||||||
rememberAsyncImagePainter(
|
|
||||||
model = url,
|
|
||||||
imageLoader = LocalContext.current.imageLoader
|
|
||||||
)
|
|
||||||
} ?: painterResource(id = message.avatar.extractResId()),
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(bottom = 6.dp)
|
|
||||||
.size(28.dp)
|
|
||||||
.alpha(if (message.showAvatar) 1f else 0f)
|
|
||||||
.clip(CircleShape),
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
Row(
|
||||||
AnimatedVisibility(visible = message.showName) {
|
modifier = Modifier
|
||||||
Text(
|
.offset { IntOffset(offsetX.roundToInt(), 0) }
|
||||||
|
.fillMaxWidth(0.85f)
|
||||||
|
.padding(start = 16.dp),
|
||||||
|
verticalAlignment = Alignment.Bottom,
|
||||||
|
horizontalArrangement = Arrangement.Start
|
||||||
|
) {
|
||||||
|
if (message.isInChat) {
|
||||||
|
Image(
|
||||||
|
painter =
|
||||||
|
message.avatar.extractUrl()?.let { url ->
|
||||||
|
rememberAsyncImagePainter(
|
||||||
|
model = url,
|
||||||
|
imageLoader = LocalContext.current.imageLoader
|
||||||
|
)
|
||||||
|
} ?: painterResource(id = message.avatar.extractResId()),
|
||||||
|
contentDescription = null,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(start = 12.dp)
|
.padding(bottom = 6.dp)
|
||||||
.widthIn(max = 140.dp),
|
.size(28.dp)
|
||||||
text = message.name,
|
.alpha(if (message.showAvatar) 1f else 0f)
|
||||||
style = MaterialTheme.typography.bodySmall,
|
.clip(CircleShape),
|
||||||
color = MaterialTheme.colorScheme.primary,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
)
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageBubble(
|
Column {
|
||||||
modifier = Modifier,
|
AnimatedVisibility(visible = message.showName) {
|
||||||
text = message.text,
|
Text(
|
||||||
isOut = false,
|
modifier = Modifier
|
||||||
date = message.date,
|
.padding(start = 12.dp)
|
||||||
isEdited = message.isEdited,
|
.widthIn(max = 140.dp),
|
||||||
isRead = message.isRead,
|
text = message.name,
|
||||||
sendingStatus = message.sendingStatus,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
isPinned = message.isPinned,
|
color = MaterialTheme.colorScheme.primary,
|
||||||
isImportant = message.isImportant,
|
maxLines = 1,
|
||||||
isSelected = message.isSelected,
|
overflow = TextOverflow.Ellipsis,
|
||||||
attachments = message.attachments?.toImmutableList(),
|
)
|
||||||
replyTitle = message.replyTitle,
|
}
|
||||||
replySummary = message.replySummary,
|
|
||||||
onClick = currentOnClick,
|
MessageBubble(
|
||||||
onLongClick = currentOnLongClick,
|
modifier = Modifier,
|
||||||
onReplyClick = currentOnReplyClick
|
text = message.text,
|
||||||
)
|
isOut = false,
|
||||||
|
date = message.date,
|
||||||
|
isEdited = message.isEdited,
|
||||||
|
isRead = message.isRead,
|
||||||
|
sendingStatus = message.sendingStatus,
|
||||||
|
isPinned = message.isPinned,
|
||||||
|
isImportant = message.isImportant,
|
||||||
|
isSelected = message.isSelected,
|
||||||
|
attachments = message.attachments?.toImmutableList(),
|
||||||
|
replyTitle = message.replyTitle,
|
||||||
|
replySummary = message.replySummary,
|
||||||
|
onClick = currentOnClick,
|
||||||
|
onLongClick = currentOnLongClick,
|
||||||
|
onReplyClick = currentOnReplyClick
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Spacer(modifier = Modifier.fillMaxWidth(0.25f))
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.fillMaxWidth(0.25f))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+68
-35
@@ -2,7 +2,6 @@ package dev.meloda.fast.messageshistory.presentation
|
|||||||
|
|
||||||
import androidx.compose.animation.AnimatedContent
|
import androidx.compose.animation.AnimatedContent
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.animateContentSize
|
|
||||||
import androidx.compose.animation.core.animateDpAsState
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
@@ -10,33 +9,36 @@ import androidx.compose.animation.scaleIn
|
|||||||
import androidx.compose.animation.scaleOut
|
import androidx.compose.animation.scaleOut
|
||||||
import androidx.compose.animation.togetherWith
|
import androidx.compose.animation.togetherWith
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.defaultMinSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.heightIn
|
||||||
import androidx.compose.foundation.layout.imeNestedScroll
|
import androidx.compose.foundation.layout.imeNestedScroll
|
||||||
import androidx.compose.foundation.layout.imePadding
|
import androidx.compose.foundation.layout.imePadding
|
||||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.text.contextmenu.builder.item
|
import androidx.compose.foundation.text.contextmenu.builder.item
|
||||||
import androidx.compose.foundation.text.contextmenu.modifier.appendTextContextMenuComponents
|
import androidx.compose.foundation.text.contextmenu.modifier.appendTextContextMenuComponents
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextField
|
|
||||||
import androidx.compose.material3.TextFieldDefaults
|
import androidx.compose.material3.TextFieldDefaults
|
||||||
import androidx.compose.material3.surfaceColorAtElevation
|
import androidx.compose.material3.surfaceColorAtElevation
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.retain.retain
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
@@ -51,6 +53,7 @@ import androidx.compose.ui.res.painterResource
|
|||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
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.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.view.HapticFeedbackConstantsCompat
|
import androidx.core.view.HapticFeedbackConstantsCompat
|
||||||
@@ -61,16 +64,16 @@ import dev.chrisbanes.haze.materials.HazeMaterials
|
|||||||
import dev.meloda.fast.datastore.AppSettings
|
import dev.meloda.fast.datastore.AppSettings
|
||||||
import dev.meloda.fast.messageshistory.model.ActionMode
|
import dev.meloda.fast.messageshistory.model.ActionMode
|
||||||
import dev.meloda.fast.ui.R
|
import dev.meloda.fast.ui.R
|
||||||
import dev.meloda.fast.ui.components.IconButton
|
import dev.meloda.fast.ui.components.FastTextField
|
||||||
|
import dev.meloda.fast.ui.components.RippledClickContainer
|
||||||
import dev.meloda.fast.ui.theme.LocalThemeConfig
|
import dev.meloda.fast.ui.theme.LocalThemeConfig
|
||||||
|
|
||||||
@OptIn(ExperimentalLayoutApi::class, ExperimentalHazeMaterialsApi::class)
|
@OptIn(ExperimentalLayoutApi::class, ExperimentalHazeMaterialsApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun MessagesHistoryInputBar(
|
fun InputBar(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
message: TextFieldValue,
|
message: TextFieldValue,
|
||||||
hazeState: HazeState,
|
hazeState: HazeState,
|
||||||
enableHaptic: Boolean,
|
|
||||||
showEmojiButton: Boolean,
|
showEmojiButton: Boolean,
|
||||||
showAttachmentButton: Boolean,
|
showAttachmentButton: Boolean,
|
||||||
actionMode: ActionMode,
|
actionMode: ActionMode,
|
||||||
@@ -93,6 +96,12 @@ fun MessagesHistoryInputBar(
|
|||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
|
|
||||||
|
val theme = LocalThemeConfig.current
|
||||||
|
|
||||||
|
var localMessage by retain(message) {
|
||||||
|
mutableStateOf(message)
|
||||||
|
}
|
||||||
|
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
|
||||||
LaunchedEffect(inputFieldFocusRequester) {
|
LaunchedEffect(inputFieldFocusRequester) {
|
||||||
@@ -101,23 +110,31 @@ fun MessagesHistoryInputBar(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val theme = LocalThemeConfig.current
|
val inputBarCornerRadius =
|
||||||
|
if (replyTitle == null) (32.dp - if (localMessage.text.lines().size > 1) 8.dp else 0.dp) else 24.dp
|
||||||
|
|
||||||
val inputBarTopCornerRadius by animateDpAsState(
|
val inputBarTopCornerRadius by animateDpAsState(
|
||||||
targetValue = if (replyTitle == null) 24.dp else 0.dp,
|
targetValue = if (replyTitle == null) inputBarCornerRadius else 0.dp,
|
||||||
label = "inputBarTopCornerRadius"
|
label = "inputBarTopCornerRadius"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val inputBarShape = RoundedCornerShape(
|
||||||
|
topStart = inputBarTopCornerRadius,
|
||||||
|
topEnd = inputBarTopCornerRadius,
|
||||||
|
bottomStart = inputBarCornerRadius,
|
||||||
|
bottomEnd = inputBarCornerRadius
|
||||||
|
)
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.background(Color.Transparent)
|
.background(Color.Transparent)
|
||||||
.padding(bottom = 8.dp)
|
|
||||||
.navigationBarsPadding()
|
.navigationBarsPadding()
|
||||||
.imePadding()
|
.imePadding()
|
||||||
) {
|
) {
|
||||||
AnimatedVisibility(replyTitle != null) {
|
AnimatedVisibility(replyTitle != null) {
|
||||||
ReplyContainer(
|
ReplyContainer(
|
||||||
|
modifier = Modifier.padding(horizontal = 8.dp),
|
||||||
title = replyTitle.orEmpty(),
|
title = replyTitle.orEmpty(),
|
||||||
text = replyText.orEmpty(),
|
text = replyText.orEmpty(),
|
||||||
onCloseClicked = onReplyCloseClicked,
|
onCloseClicked = onReplyCloseClicked,
|
||||||
@@ -127,35 +144,24 @@ fun MessagesHistoryInputBar(
|
|||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.defaultMinSize(minHeight = 60.dp)
|
.heightIn(min = 48.dp)
|
||||||
.imeNestedScroll(),
|
.imeNestedScroll(),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clip(
|
.clip(inputBarShape)
|
||||||
RoundedCornerShape(
|
|
||||||
topStart = inputBarTopCornerRadius,
|
|
||||||
topEnd = inputBarTopCornerRadius,
|
|
||||||
bottomStart = 24.dp,
|
|
||||||
bottomEnd = 24.dp
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.then(
|
.then(
|
||||||
if (theme.enableBlur) {
|
if (theme.enableBlur) {
|
||||||
Modifier
|
Modifier
|
||||||
.hazeEffect(
|
.hazeEffect(
|
||||||
state = hazeState,
|
state = hazeState,
|
||||||
style = HazeMaterials.ultraThin()
|
style = HazeMaterials.thin()
|
||||||
)
|
|
||||||
.border(
|
|
||||||
1.dp, MaterialTheme.colorScheme.outlineVariant,
|
|
||||||
RoundedCornerShape(36.dp)
|
|
||||||
)
|
)
|
||||||
} else Modifier
|
} else Modifier
|
||||||
)
|
)
|
||||||
.animateContentSize()
|
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.background(
|
.background(
|
||||||
if (theme.enableBlur) Color.Transparent
|
if (theme.enableBlur) Color.Transparent
|
||||||
@@ -172,7 +178,9 @@ fun MessagesHistoryInputBar(
|
|||||||
|
|
||||||
if (showEmojiButton) {
|
if (showEmojiButton) {
|
||||||
Column(verticalArrangement = Arrangement.Bottom) {
|
Column(verticalArrangement = Arrangement.Bottom) {
|
||||||
IconButton(
|
RippledClickContainer(
|
||||||
|
modifier = Modifier.size(36.dp),
|
||||||
|
shape = CircleShape,
|
||||||
onClick = {
|
onClick = {
|
||||||
if (AppSettings.General.enableHaptic) {
|
if (AppSettings.General.enableHaptic) {
|
||||||
view.performHapticFeedback(
|
view.performHapticFeedback(
|
||||||
@@ -196,14 +204,15 @@ fun MessagesHistoryInputBar(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(6.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TextField(
|
FastTextField(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.focusRequester(focusRequester)
|
.focusRequester(focusRequester)
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
|
.heightIn(min = 48.dp)
|
||||||
.appendTextContextMenuComponents {
|
.appendTextContextMenuComponents {
|
||||||
separator()
|
separator()
|
||||||
|
|
||||||
@@ -245,8 +254,11 @@ fun MessagesHistoryInputBar(
|
|||||||
|
|
||||||
separator()
|
separator()
|
||||||
},
|
},
|
||||||
value = message,
|
value = localMessage,
|
||||||
onValueChange = onMessageInputChanged,
|
onValueChange = { newValue ->
|
||||||
|
localMessage = newValue
|
||||||
|
onMessageInputChanged(newValue)
|
||||||
|
},
|
||||||
colors = TextFieldDefaults.colors(
|
colors = TextFieldDefaults.colors(
|
||||||
unfocusedContainerColor = Color.Transparent,
|
unfocusedContainerColor = Color.Transparent,
|
||||||
focusedContainerColor = Color.Transparent,
|
focusedContainerColor = Color.Transparent,
|
||||||
@@ -264,10 +276,12 @@ fun MessagesHistoryInputBar(
|
|||||||
|
|
||||||
if (showAttachmentButton) {
|
if (showAttachmentButton) {
|
||||||
Column(verticalArrangement = Arrangement.Bottom) {
|
Column(verticalArrangement = Arrangement.Bottom) {
|
||||||
IconButton(
|
RippledClickContainer(
|
||||||
|
modifier = Modifier.size(36.dp),
|
||||||
|
shape = CircleShape,
|
||||||
onClick = {
|
onClick = {
|
||||||
onAttachmentButtonClicked()
|
onAttachmentButtonClicked()
|
||||||
if (enableHaptic) {
|
if (AppSettings.General.enableHaptic) {
|
||||||
view.performHapticFeedback(
|
view.performHapticFeedback(
|
||||||
HapticFeedbackConstantsCompat.REJECT
|
HapticFeedbackConstantsCompat.REJECT
|
||||||
)
|
)
|
||||||
@@ -281,12 +295,16 @@ fun MessagesHistoryInputBar(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(6.dp))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
}
|
}
|
||||||
|
|
||||||
Column(verticalArrangement = Arrangement.Bottom) {
|
Column(verticalArrangement = Arrangement.Bottom) {
|
||||||
IconButton(
|
RippledClickContainer(
|
||||||
|
modifier = Modifier.size(36.dp),
|
||||||
|
shape = CircleShape,
|
||||||
onClick = {
|
onClick = {
|
||||||
onActionButtonClicked()
|
onActionButtonClicked()
|
||||||
if (AppSettings.General.enableHaptic && actionMode.isRecord()) {
|
if (AppSettings.General.enableHaptic && actionMode.isRecord()) {
|
||||||
@@ -318,7 +336,7 @@ fun MessagesHistoryInputBar(
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(6.dp))
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.width(6.dp))
|
Spacer(modifier = Modifier.width(6.dp))
|
||||||
@@ -328,3 +346,18 @@ fun MessagesHistoryInputBar(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
private fun InputBarPreview() {
|
||||||
|
InputBar(
|
||||||
|
message = TextFieldValue("Привет!"),
|
||||||
|
hazeState = remember { HazeState() },
|
||||||
|
showEmojiButton = true,
|
||||||
|
showAttachmentButton = true,
|
||||||
|
actionMode = ActionMode.SEND,
|
||||||
|
replyTitle = "Иннокентий Панфилович",
|
||||||
|
replyText = "Ого, ром!",
|
||||||
|
inputFieldFocusRequester = false
|
||||||
|
)
|
||||||
|
}
|
||||||
+2
-2
@@ -55,7 +55,6 @@ fun MessagesHistoryRoute(
|
|||||||
canPaginate = canPaginate,
|
canPaginate = canPaginate,
|
||||||
showEmojiButton = AppSettings.General.showEmojiButton,
|
showEmojiButton = AppSettings.General.showEmojiButton,
|
||||||
showAttachmentButton = AppSettings.General.showAttachmentButton,
|
showAttachmentButton = AppSettings.General.showAttachmentButton,
|
||||||
enableHaptic = AppSettings.General.enableHaptic,
|
|
||||||
inputFieldFocusRequester = inputFieldFocusRequester,
|
inputFieldFocusRequester = inputFieldFocusRequester,
|
||||||
onBack = onBack,
|
onBack = onBack,
|
||||||
onClose = viewModel::onCloseButtonClicked,
|
onClose = viewModel::onCloseButtonClicked,
|
||||||
@@ -79,7 +78,8 @@ fun MessagesHistoryRoute(
|
|||||||
onUnderlineRequested = viewModel::onUnderlineClicked,
|
onUnderlineRequested = viewModel::onUnderlineClicked,
|
||||||
onLinkRequested = viewModel::onLinkClicked,
|
onLinkRequested = viewModel::onLinkClicked,
|
||||||
onRegularRequested = viewModel::onRegularClicked,
|
onRegularRequested = viewModel::onRegularClicked,
|
||||||
onReplyCloseClicked = viewModel::onReplyCloseClicked
|
onReplyCloseClicked = viewModel::onReplyCloseClicked,
|
||||||
|
onRequestReplyToMessage = viewModel::onRequestReplyToMessage,
|
||||||
)
|
)
|
||||||
|
|
||||||
HandleDialogs(
|
HandleDialogs(
|
||||||
|
|||||||
+7
-5
@@ -69,7 +69,6 @@ fun MessagesHistoryScreen(
|
|||||||
canPaginate: Boolean = false,
|
canPaginate: Boolean = false,
|
||||||
showEmojiButton: Boolean = false,
|
showEmojiButton: Boolean = false,
|
||||||
showAttachmentButton: Boolean = false,
|
showAttachmentButton: Boolean = false,
|
||||||
enableHaptic: Boolean = false,
|
|
||||||
inputFieldFocusRequester: Boolean,
|
inputFieldFocusRequester: Boolean,
|
||||||
onBack: () -> Unit = {},
|
onBack: () -> Unit = {},
|
||||||
onClose: () -> Unit = {},
|
onClose: () -> Unit = {},
|
||||||
@@ -94,6 +93,7 @@ fun MessagesHistoryScreen(
|
|||||||
onUnderlineRequested: () -> Unit = {},
|
onUnderlineRequested: () -> Unit = {},
|
||||||
onRegularRequested: () -> Unit = {},
|
onRegularRequested: () -> Unit = {},
|
||||||
onReplyCloseClicked: () -> Unit = {},
|
onReplyCloseClicked: () -> Unit = {},
|
||||||
|
onRequestReplyToMessage: (cmId: Long) -> Unit = {}
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val view = LocalView.current
|
val view = LocalView.current
|
||||||
@@ -238,11 +238,14 @@ fun MessagesHistoryScreen(
|
|||||||
currentOnMessageClicked.invoke(id)
|
currentOnMessageClicked.invoke(id)
|
||||||
},
|
},
|
||||||
onMessageLongClicked = onMessageLongClicked,
|
onMessageLongClicked = onMessageLongClicked,
|
||||||
onPhotoClicked = onPhotoClicked
|
onPhotoClicked = onPhotoClicked,
|
||||||
|
onRequestMessageReply = onRequestReplyToMessage
|
||||||
)
|
)
|
||||||
|
|
||||||
MessagesHistoryInputBar(
|
InputBar(
|
||||||
modifier = Modifier.align(Alignment.BottomStart),
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomStart)
|
||||||
|
.padding(bottom = 8.dp),
|
||||||
message = screenState.message,
|
message = screenState.message,
|
||||||
onMessageInputChanged = onMessageInputChanged,
|
onMessageInputChanged = onMessageInputChanged,
|
||||||
onBoldRequested = onBoldRequested,
|
onBoldRequested = onBoldRequested,
|
||||||
@@ -251,7 +254,6 @@ fun MessagesHistoryScreen(
|
|||||||
onLinkRequested = onLinkRequested,
|
onLinkRequested = onLinkRequested,
|
||||||
onRegularRequested = onRegularRequested,
|
onRegularRequested = onRegularRequested,
|
||||||
hazeState = hazeState,
|
hazeState = hazeState,
|
||||||
enableHaptic = enableHaptic,
|
|
||||||
showEmojiButton = showEmojiButton,
|
showEmojiButton = showEmojiButton,
|
||||||
showAttachmentButton = showAttachmentButton,
|
showAttachmentButton = showAttachmentButton,
|
||||||
actionMode = screenState.actionMode,
|
actionMode = screenState.actionMode,
|
||||||
|
|||||||
+1
-2
@@ -88,11 +88,10 @@ fun MessagesHistoryTopBarContainer(
|
|||||||
if (showPinnedContainer) {
|
if (showPinnedContainer) {
|
||||||
PinnedMessageContainer(
|
PinnedMessageContainer(
|
||||||
modifier = Modifier,
|
modifier = Modifier,
|
||||||
pinnedMessage = requireNotNull(pinnedMessage),
|
|
||||||
title = pinnedTitle.orDots(),
|
title = pinnedTitle.orDots(),
|
||||||
summary = pinnedSummary,
|
summary = pinnedSummary,
|
||||||
canChangePin = showUnpinButton,
|
canChangePin = showUnpinButton,
|
||||||
onPinnedMessageClicked = onPinnedMessageClicked,
|
onPinnedMessageClicked = { onPinnedMessageClicked(pinnedMessage?.id ?: -1) },
|
||||||
onUnpinMessageButtonClicked = onUnpinMessageButtonClicked
|
onUnpinMessageButtonClicked = onUnpinMessageButtonClicked
|
||||||
)
|
)
|
||||||
HorizontalDivider()
|
HorizontalDivider()
|
||||||
|
|||||||
+63
-5
@@ -5,8 +5,10 @@ import android.util.Log
|
|||||||
import android.view.HapticFeedbackConstants
|
import android.view.HapticFeedbackConstants
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.animateColorAsState
|
import androidx.compose.animation.animateColorAsState
|
||||||
|
import androidx.compose.animation.core.Animatable
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.combinedClickable
|
import androidx.compose.foundation.combinedClickable
|
||||||
|
import androidx.compose.foundation.gestures.detectHorizontalDragGestures
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
@@ -22,16 +24,23 @@ import androidx.compose.material3.CircularProgressIndicator
|
|||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.rememberUpdatedState
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
|
import androidx.compose.runtime.snapshotFlow
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalView
|
import androidx.compose.ui.platform.LocalView
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
|
import androidx.core.view.HapticFeedbackConstantsCompat
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import dev.chrisbanes.haze.HazeState
|
import dev.chrisbanes.haze.HazeState
|
||||||
import dev.chrisbanes.haze.hazeSource
|
import dev.chrisbanes.haze.hazeSource
|
||||||
import dev.meloda.fast.datastore.AppSettings
|
import dev.meloda.fast.datastore.AppSettings
|
||||||
@@ -42,6 +51,8 @@ import dev.meloda.fast.model.api.domain.VkLinkDomain
|
|||||||
import dev.meloda.fast.model.api.domain.VkPhotoDomain
|
import dev.meloda.fast.model.api.domain.VkPhotoDomain
|
||||||
import dev.meloda.fast.ui.theme.LocalThemeConfig
|
import dev.meloda.fast.ui.theme.LocalThemeConfig
|
||||||
import dev.meloda.fast.ui.util.ImmutableList
|
import dev.meloda.fast.ui.util.ImmutableList
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -58,12 +69,15 @@ fun MessagesList(
|
|||||||
onRequestScrollToCmId: (cmId: Long) -> Unit = {},
|
onRequestScrollToCmId: (cmId: Long) -> Unit = {},
|
||||||
onMessageClicked: (Long) -> Unit = {},
|
onMessageClicked: (Long) -> Unit = {},
|
||||||
onMessageLongClicked: (Long) -> Unit = {},
|
onMessageLongClicked: (Long) -> Unit = {},
|
||||||
onPhotoClicked: (images: List<String>, index: Int) -> Unit = { _, _ -> }
|
onPhotoClicked: (images: List<String>, index: Int) -> Unit = { _, _ -> },
|
||||||
|
onRequestMessageReply: (cmId: Long) -> Unit = {}
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val theme = LocalThemeConfig.current
|
val theme = LocalThemeConfig.current
|
||||||
val view = LocalView.current
|
val view = LocalView.current
|
||||||
|
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
val onAttachmentClick by rememberUpdatedState(
|
val onAttachmentClick by rememberUpdatedState(
|
||||||
{ message: UiItem.Message, attachment: VkAttachment ->
|
{ message: UiItem.Message, attachment: VkAttachment ->
|
||||||
if (isSelectedAtLeastOne) {
|
if (isSelectedAtLeastOne) {
|
||||||
@@ -137,7 +151,7 @@ fun MessagesList(
|
|||||||
Spacer(modifier = Modifier.height(48.dp))
|
Spacer(modifier = Modifier.height(48.dp))
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(messageBarHeight.plus(18.dp)))
|
Spacer(modifier = Modifier.height(messageBarHeight.plus(12.dp)))
|
||||||
Spacer(
|
Spacer(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -183,6 +197,19 @@ fun MessagesList(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val offsetX = remember { Animatable(0f) }
|
||||||
|
|
||||||
|
val offsetDistinct by snapshotFlow { offsetX.value }
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.collectAsStateWithLifecycle(offsetX)
|
||||||
|
|
||||||
|
LaunchedEffect(offsetDistinct) {
|
||||||
|
if (offsetDistinct == -100f && AppSettings.General.enableHaptic) {
|
||||||
|
view.performHapticFeedback(HapticFeedbackConstantsCompat.CONTEXT_CLICK)
|
||||||
|
}
|
||||||
|
Log.d("MessagesList", "offsetDistinct: $offsetDistinct")
|
||||||
|
}
|
||||||
|
|
||||||
Surface(
|
Surface(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.then(
|
.then(
|
||||||
@@ -199,7 +226,36 @@ fun MessagesList(
|
|||||||
onMessageLongClicked(item.id)
|
onMessageLongClicked(item.id)
|
||||||
},
|
},
|
||||||
onClick = { onMessageClicked(item.id) }
|
onClick = { onMessageClicked(item.id) }
|
||||||
),
|
)
|
||||||
|
.pointerInput(Unit) {
|
||||||
|
detectHorizontalDragGestures(
|
||||||
|
onDragCancel = {
|
||||||
|
if (offsetX.value == -100f) {
|
||||||
|
onRequestMessageReply(item.cmId)
|
||||||
|
}
|
||||||
|
|
||||||
|
scope.launch {
|
||||||
|
offsetX.animateTo(0f)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDragEnd = {
|
||||||
|
if (offsetX.value == -100f) {
|
||||||
|
onRequestMessageReply(item.cmId)
|
||||||
|
}
|
||||||
|
|
||||||
|
scope.launch {
|
||||||
|
offsetX.animateTo(0f)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onHorizontalDrag = { _, dragAmount ->
|
||||||
|
scope.launch {
|
||||||
|
offsetX.snapTo(
|
||||||
|
(offsetX.value + dragAmount).coerceIn(-100f, 0f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
color = backgroundColor
|
color = backgroundColor
|
||||||
) {
|
) {
|
||||||
if (item.isOut) {
|
if (item.isOut) {
|
||||||
@@ -226,7 +282,8 @@ fun MessagesList(
|
|||||||
if (item.replyCmId != null) {
|
if (item.replyCmId != null) {
|
||||||
onRequestScrollToCmId(item.replyCmId)
|
onRequestScrollToCmId(item.replyCmId)
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
offsetX = offsetX.value
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
IncomingMessageBubble(
|
IncomingMessageBubble(
|
||||||
@@ -252,7 +309,8 @@ fun MessagesList(
|
|||||||
if (item.replyCmId != null) {
|
if (item.replyCmId != null) {
|
||||||
onRequestScrollToCmId(item.replyCmId)
|
onRequestScrollToCmId(item.replyCmId)
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
offsetX = offsetX.value
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+48
-23
@@ -2,24 +2,34 @@ package dev.meloda.fast.messageshistory.presentation
|
|||||||
|
|
||||||
import androidx.compose.animation.animateContentSize
|
import androidx.compose.animation.animateContentSize
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.offset
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.rememberUpdatedState
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.unit.IntOffset
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.conena.nanokt.android.content.dpInPx
|
||||||
import dev.meloda.fast.messageshistory.model.UiItem
|
import dev.meloda.fast.messageshistory.model.UiItem
|
||||||
import dev.meloda.fast.model.api.domain.VkAttachment
|
import dev.meloda.fast.model.api.domain.VkAttachment
|
||||||
|
import dev.meloda.fast.ui.R
|
||||||
import dev.meloda.fast.ui.util.ImmutableList.Companion.toImmutableList
|
import dev.meloda.fast.ui.util.ImmutableList.Companion.toImmutableList
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun OutgoingMessageBubble(
|
fun OutgoingMessageBubble(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
enableAnimations: Boolean,
|
enableAnimations: Boolean,
|
||||||
message: UiItem.Message,
|
message: UiItem.Message,
|
||||||
|
offsetX: Float = 0f,
|
||||||
onClick: (VkAttachment) -> Unit = {},
|
onClick: (VkAttachment) -> Unit = {},
|
||||||
onLongClick: (VkAttachment) -> Unit = {},
|
onLongClick: (VkAttachment) -> Unit = {},
|
||||||
onReplyClick: () -> Unit = {}
|
onReplyClick: () -> Unit = {}
|
||||||
@@ -38,31 +48,46 @@ fun OutgoingMessageBubble(
|
|||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.End
|
horizontalArrangement = Arrangement.End
|
||||||
) {
|
) {
|
||||||
Row(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxWidth(),
|
||||||
.padding(end = 16.dp)
|
contentAlignment = Alignment.CenterEnd
|
||||||
.fillMaxWidth(0.85f),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.End
|
|
||||||
) {
|
) {
|
||||||
MessageBubble(
|
Icon(
|
||||||
modifier = Modifier,
|
painter = painterResource(R.drawable.round_reply_24),
|
||||||
text = message.text,
|
contentDescription = null,
|
||||||
isOut = true,
|
tint = MaterialTheme.colorScheme.secondary,
|
||||||
date = message.date,
|
modifier = Modifier
|
||||||
isEdited = message.isEdited,
|
.align(Alignment.CenterEnd)
|
||||||
isRead = message.isRead,
|
.offset { IntOffset(24.dpInPx + offsetX.roundToInt(), y = 0) }
|
||||||
sendingStatus = message.sendingStatus,
|
|
||||||
isPinned = message.isPinned,
|
|
||||||
isImportant = message.isImportant,
|
|
||||||
isSelected = message.isSelected,
|
|
||||||
attachments = message.attachments?.toImmutableList(),
|
|
||||||
replyTitle = message.replyTitle,
|
|
||||||
replySummary = message.replySummary,
|
|
||||||
onClick = currentOnClick,
|
|
||||||
onLongClick = currentOnLongClick,
|
|
||||||
onReplyClick = currentOnReplyClick
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.offset { IntOffset(offsetX.roundToInt(), 0) }
|
||||||
|
.padding(end = 16.dp)
|
||||||
|
.fillMaxWidth(0.85f),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.End
|
||||||
|
) {
|
||||||
|
MessageBubble(
|
||||||
|
modifier = Modifier,
|
||||||
|
text = message.text,
|
||||||
|
isOut = true,
|
||||||
|
date = message.date,
|
||||||
|
isEdited = message.isEdited,
|
||||||
|
isRead = message.isRead,
|
||||||
|
sendingStatus = message.sendingStatus,
|
||||||
|
isPinned = message.isPinned,
|
||||||
|
isImportant = message.isImportant,
|
||||||
|
isSelected = message.isSelected,
|
||||||
|
attachments = message.attachments?.toImmutableList(),
|
||||||
|
replyTitle = message.replyTitle,
|
||||||
|
replySummary = message.replySummary,
|
||||||
|
onClick = currentOnClick,
|
||||||
|
onLongClick = currentOnLongClick,
|
||||||
|
onReplyClick = currentOnReplyClick
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+46
-23
@@ -1,15 +1,20 @@
|
|||||||
package dev.meloda.fast.messageshistory.presentation
|
package dev.meloda.fast.messageshistory.presentation
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
@@ -18,49 +23,48 @@ import androidx.compose.ui.draw.alpha
|
|||||||
import androidx.compose.ui.draw.rotate
|
import androidx.compose.ui.draw.rotate
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
import androidx.compose.ui.text.buildAnnotatedString
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
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.R
|
||||||
import dev.meloda.fast.ui.basic.ContentAlpha
|
import dev.meloda.fast.ui.basic.ContentAlpha
|
||||||
import dev.meloda.fast.ui.basic.LocalContentAlpha
|
import dev.meloda.fast.ui.basic.LocalContentAlpha
|
||||||
import dev.meloda.fast.ui.components.IconButton
|
import dev.meloda.fast.ui.components.RippledClickContainer
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PinnedMessageContainer(
|
fun PinnedMessageContainer(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
pinnedMessage: VkMessage,
|
|
||||||
title: String,
|
title: String,
|
||||||
summary: AnnotatedString?,
|
summary: AnnotatedString?,
|
||||||
canChangePin: Boolean,
|
canChangePin: Boolean,
|
||||||
onPinnedMessageClicked: (Long) -> Unit = {},
|
onPinnedMessageClicked: () -> Unit = {},
|
||||||
onUnpinMessageButtonClicked: () -> Unit = {}
|
onUnpinMessageButtonClicked: () -> Unit = {}
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(56.dp)
|
.height(42.dp)
|
||||||
.clickable { onPinnedMessageClicked(pinnedMessage.id) }
|
.clickable(onClick = onPinnedMessageClicked)
|
||||||
.padding(start = 16.dp),
|
.padding(start = 12.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.ic_round_push_pin_24),
|
||||||
|
contentDescription = null,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.rotate(45f)
|
.rotate(45f)
|
||||||
.alpha(0.5f),
|
.alpha(0.5f),
|
||||||
painter = painterResource(R.drawable.ic_round_push_pin_24),
|
|
||||||
contentDescription = null
|
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.width(16.dp))
|
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = title,
|
text = title,
|
||||||
fontWeight = FontWeight.Medium,
|
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.Medium),
|
||||||
color = MaterialTheme.colorScheme.primary,
|
color = MaterialTheme.colorScheme.primary,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis
|
||||||
@@ -69,6 +73,7 @@ fun PinnedMessageContainer(
|
|||||||
LocalContentAlpha(alpha = ContentAlpha.medium) {
|
LocalContentAlpha(alpha = ContentAlpha.medium) {
|
||||||
Text(
|
Text(
|
||||||
text = summary,
|
text = summary,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
@@ -76,18 +81,36 @@ fun PinnedMessageContainer(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (canChangePin) {
|
AnimatedVisibility(canChangePin) {
|
||||||
Spacer(modifier = Modifier.width(16.dp))
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
RippledClickContainer(
|
||||||
|
modifier = Modifier.size(36.dp),
|
||||||
|
shape = CircleShape,
|
||||||
|
onClick = onUnpinMessageButtonClicked
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.round_close_24px),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.alpha(0.5f),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
IconButton(onClick = onUnpinMessageButtonClicked) {
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
Icon(
|
|
||||||
modifier = Modifier.alpha(0.5f),
|
|
||||||
painter = painterResource(R.drawable.round_close_24px),
|
|
||||||
contentDescription = null
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
private fun PinnedMessageContainerPreview() {
|
||||||
|
Surface {
|
||||||
|
PinnedMessageContainer(
|
||||||
|
title = "Иннокентий Панфилович",
|
||||||
|
summary = buildAnnotatedString { append("Здравствуйте, как Ваше ничего?") },
|
||||||
|
canChangePin = true,
|
||||||
|
onPinnedMessageClicked = {},
|
||||||
|
onUnpinMessageButtonClicked = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+12
-17
@@ -3,10 +3,8 @@ package dev.meloda.fast.messageshistory.presentation
|
|||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.heightIn
|
import androidx.compose.foundation.layout.heightIn
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
@@ -15,6 +13,7 @@ import androidx.compose.foundation.shape.CircleShape
|
|||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.surfaceColorAtElevation
|
import androidx.compose.material3.surfaceColorAtElevation
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -31,25 +30,24 @@ import dev.meloda.fast.ui.components.RippledClickContainer
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ReplyContainer(
|
fun ReplyContainer(
|
||||||
onCloseClicked: () -> Unit = {},
|
|
||||||
title: String,
|
title: String,
|
||||||
text: String?,
|
text: String?,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
onCloseClicked: () -> Unit = {},
|
||||||
backgroundColor: Color = MaterialTheme.colorScheme.surfaceColorAtElevation(5.dp)
|
backgroundColor: Color = MaterialTheme.colorScheme.surfaceColorAtElevation(5.dp)
|
||||||
) {
|
) {
|
||||||
|
val shape = RoundedCornerShape(
|
||||||
|
topStart = 24.dp,
|
||||||
|
topEnd = 24.dp,
|
||||||
|
bottomStart = 0.dp,
|
||||||
|
bottomEnd = 0.dp
|
||||||
|
)
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.padding(horizontal = 8.dp)
|
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.heightIn(min = 48.dp)
|
.heightIn(min = 48.dp)
|
||||||
.clip(
|
.clip(shape)
|
||||||
RoundedCornerShape(
|
|
||||||
topStart = 24.dp,
|
|
||||||
topEnd = 24.dp,
|
|
||||||
bottomStart = 0.dp,
|
|
||||||
bottomEnd = 0.dp
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.background(backgroundColor)
|
.background(backgroundColor)
|
||||||
.padding(horizontal = 8.dp),
|
.padding(horizontal = 8.dp),
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
@@ -99,11 +97,8 @@ fun ReplyContainer(
|
|||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
private fun ReplyContainerPreview() {
|
private fun ReplyContainerPreview() {
|
||||||
Box(
|
Surface(
|
||||||
modifier = Modifier
|
modifier = Modifier,
|
||||||
.fillMaxSize()
|
|
||||||
.background(Color.White),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
) {
|
||||||
ReplyContainer(
|
ReplyContainer(
|
||||||
onCloseClicked = {},
|
onCloseClicked = {},
|
||||||
|
|||||||
+2
-2
@@ -25,7 +25,7 @@ import androidx.compose.ui.unit.Dp
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.times
|
import androidx.compose.ui.unit.times
|
||||||
import dev.meloda.fast.ui.R
|
import dev.meloda.fast.ui.R
|
||||||
import dev.meloda.fast.ui.components.IconButton
|
import dev.meloda.fast.ui.components.FastIconButton
|
||||||
import kotlin.collections.forEachIndexed
|
import kotlin.collections.forEachIndexed
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -80,7 +80,7 @@ fun AudioMessage(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
IconButton(
|
FastIconButton(
|
||||||
onClick = onPlayClick,
|
onClick = onPlayClick,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
|
|||||||
+2
-2
@@ -29,7 +29,7 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
import dev.meloda.fast.ui.R
|
import dev.meloda.fast.ui.R
|
||||||
import dev.meloda.fast.ui.components.IconButton
|
import dev.meloda.fast.ui.components.FastIconButton
|
||||||
import dev.meloda.fast.ui.util.ImmutableList
|
import dev.meloda.fast.ui.util.ImmutableList
|
||||||
import dev.meloda.fast.ui.util.ImmutableList.Companion.toImmutableList
|
import dev.meloda.fast.ui.util.ImmutableList.Companion.toImmutableList
|
||||||
|
|
||||||
@@ -101,7 +101,7 @@ fun DynamicPreviewGrid(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (preview.isVideo) {
|
if (preview.isVideo) {
|
||||||
IconButton(
|
FastIconButton(
|
||||||
onClick = { currentOnClick(index) },
|
onClick = { currentOnClick(index) },
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(36.dp)
|
.size(36.dp)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ kotlin = "2.2.21"
|
|||||||
ksp = "2.3.3"
|
ksp = "2.3.3"
|
||||||
moduleGraph = "2.9.0"
|
moduleGraph = "2.9.0"
|
||||||
|
|
||||||
compose-bom = "2025.11.01"
|
compose-bom = "2025.12.00"
|
||||||
koin = "4.1.1"
|
koin = "4.1.1"
|
||||||
|
|
||||||
accompanist = "0.37.3"
|
accompanist = "0.37.3"
|
||||||
|
|||||||
Reference in New Issue
Block a user