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)
|
||||
@Composable
|
||||
fun IconButton(
|
||||
fun FastIconButton(
|
||||
modifier: Modifier = Modifier,
|
||||
onClick: () -> Unit = {},
|
||||
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
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.ripple
|
||||
@@ -16,15 +16,17 @@ fun RippledClickContainer(
|
||||
modifier: Modifier = Modifier,
|
||||
shape: Shape = RoundedCornerShape(4.dp),
|
||||
onClick: () -> Unit,
|
||||
onLongClick: (() -> Unit)? = null,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.clip(shape)
|
||||
.clickable(
|
||||
.combinedClickable(
|
||||
interactionSource = null,
|
||||
indication = ripple(),
|
||||
onClick = onClick
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick
|
||||
),
|
||||
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.ui.R
|
||||
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.NoItemsView
|
||||
import dev.meloda.fast.ui.components.VkErrorView
|
||||
@@ -205,7 +205,7 @@ fun CreateChatScreen(
|
||||
) {
|
||||
TopAppBar(
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
FastIconButton(onClick = onBack) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.round_arrow_back_24px),
|
||||
contentDescription = null
|
||||
|
||||
+2
@@ -63,4 +63,6 @@ interface MessagesHistoryViewModel {
|
||||
fun onRegularClicked()
|
||||
|
||||
fun onReplyCloseClicked()
|
||||
|
||||
fun onRequestReplyToMessage(cmId: Long)
|
||||
}
|
||||
|
||||
+21
-22
@@ -61,7 +61,6 @@ import dev.meloda.fast.network.VkErrorCode
|
||||
import dev.meloda.fast.ui.R
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -235,22 +234,7 @@ class MessagesHistoryViewModelImpl(
|
||||
// TODO: 28-Mar-25, Danil Nikolaev: retry sending
|
||||
}
|
||||
|
||||
MessageOption.Reply -> {
|
||||
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.Reply -> replyToMessage(cmId)
|
||||
|
||||
MessageOption.ForwardHere -> {
|
||||
|
||||
@@ -349,14 +333,12 @@ class MessagesHistoryViewModelImpl(
|
||||
|
||||
override fun onEmojiButtonLongClicked() {
|
||||
AppSettings.Features.fastText.takeIf { it.isNotBlank() }?.let { text ->
|
||||
screenState.setValue { old ->
|
||||
val newText = "${old.message.text}$text"
|
||||
old.copy(
|
||||
message = TextFieldValue(text = newText, selection = TextRange(newText.length))
|
||||
val newText = "${screenState.value.message.text}$text"
|
||||
onMessageInputChanged(
|
||||
TextFieldValue(text = newText, selection = TextRange(newText.length))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActionButtonClicked() {
|
||||
when (screenState.value.actionMode) {
|
||||
@@ -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 fun updateStyles() {
|
||||
@@ -576,6 +571,10 @@ class MessagesHistoryViewModelImpl(
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRequestReplyToMessage(cmId: Long) {
|
||||
replyToMessage(cmId)
|
||||
}
|
||||
|
||||
private fun handleNewMessage(event: LongPollParsedEvent.NewMessage) {
|
||||
val message = event.message
|
||||
|
||||
|
||||
+21
@@ -4,15 +4,18 @@ import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
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.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
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.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil.compose.rememberAsyncImagePainter
|
||||
import coil.imageLoader
|
||||
import com.conena.nanokt.android.content.dpInPx
|
||||
import dev.meloda.fast.messageshistory.model.UiItem
|
||||
import dev.meloda.fast.model.api.domain.VkAttachment
|
||||
import dev.meloda.fast.ui.R
|
||||
import dev.meloda.fast.ui.util.ImmutableList.Companion.toImmutableList
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Composable
|
||||
fun IncomingMessageBubble(
|
||||
enableAnimations: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
message: UiItem.Message,
|
||||
offsetX: Float = 0f,
|
||||
onClick: (VkAttachment) -> Unit = {},
|
||||
onLongClick: (VkAttachment) -> Unit = {},
|
||||
onReplyClick: () -> Unit = {}
|
||||
@@ -53,8 +61,19 @@ fun IncomingMessageBubble(
|
||||
else Modifier
|
||||
),
|
||||
) {
|
||||
Box(modifier = Modifier.fillMaxWidth()) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.round_reply_24),
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.secondary,
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterEnd)
|
||||
.offset { IntOffset(24.dpInPx + offsetX.roundToInt(), y = 0) }
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.offset { IntOffset(offsetX.roundToInt(), 0) }
|
||||
.fillMaxWidth(0.85f)
|
||||
.padding(start = 16.dp),
|
||||
verticalAlignment = Alignment.Bottom,
|
||||
@@ -111,8 +130,10 @@ fun IncomingMessageBubble(
|
||||
onLongClick = currentOnLongClick,
|
||||
onReplyClick = currentOnReplyClick
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
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.AnimatedVisibility
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
@@ -10,33 +9,36 @@ import androidx.compose.animation.scaleIn
|
||||
import androidx.compose.animation.scaleOut
|
||||
import androidx.compose.animation.togetherWith
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.imeNestedScroll
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.contextmenu.builder.item
|
||||
import androidx.compose.foundation.text.contextmenu.modifier.appendTextContextMenuComponents
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.material3.TextFieldDefaults
|
||||
import androidx.compose.material3.surfaceColorAtElevation
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
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.Modifier
|
||||
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.text.input.TextFieldValue
|
||||
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.core.view.HapticFeedbackConstantsCompat
|
||||
@@ -61,16 +64,16 @@ import dev.chrisbanes.haze.materials.HazeMaterials
|
||||
import dev.meloda.fast.datastore.AppSettings
|
||||
import dev.meloda.fast.messageshistory.model.ActionMode
|
||||
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
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class, ExperimentalHazeMaterialsApi::class)
|
||||
@Composable
|
||||
fun MessagesHistoryInputBar(
|
||||
fun InputBar(
|
||||
modifier: Modifier = Modifier,
|
||||
message: TextFieldValue,
|
||||
hazeState: HazeState,
|
||||
enableHaptic: Boolean,
|
||||
showEmojiButton: Boolean,
|
||||
showAttachmentButton: Boolean,
|
||||
actionMode: ActionMode,
|
||||
@@ -93,6 +96,12 @@ fun MessagesHistoryInputBar(
|
||||
val context = LocalContext.current
|
||||
val density = LocalDensity.current
|
||||
|
||||
val theme = LocalThemeConfig.current
|
||||
|
||||
var localMessage by retain(message) {
|
||||
mutableStateOf(message)
|
||||
}
|
||||
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
|
||||
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(
|
||||
targetValue = if (replyTitle == null) 24.dp else 0.dp,
|
||||
targetValue = if (replyTitle == null) inputBarCornerRadius else 0.dp,
|
||||
label = "inputBarTopCornerRadius"
|
||||
)
|
||||
|
||||
val inputBarShape = RoundedCornerShape(
|
||||
topStart = inputBarTopCornerRadius,
|
||||
topEnd = inputBarTopCornerRadius,
|
||||
bottomStart = inputBarCornerRadius,
|
||||
bottomEnd = inputBarCornerRadius
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.background(Color.Transparent)
|
||||
.padding(bottom = 8.dp)
|
||||
.navigationBarsPadding()
|
||||
.imePadding()
|
||||
) {
|
||||
AnimatedVisibility(replyTitle != null) {
|
||||
ReplyContainer(
|
||||
modifier = Modifier.padding(horizontal = 8.dp),
|
||||
title = replyTitle.orEmpty(),
|
||||
text = replyText.orEmpty(),
|
||||
onCloseClicked = onReplyCloseClicked,
|
||||
@@ -127,35 +144,24 @@ fun MessagesHistoryInputBar(
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.defaultMinSize(minHeight = 60.dp)
|
||||
.heightIn(min = 48.dp)
|
||||
.imeNestedScroll(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.clip(
|
||||
RoundedCornerShape(
|
||||
topStart = inputBarTopCornerRadius,
|
||||
topEnd = inputBarTopCornerRadius,
|
||||
bottomStart = 24.dp,
|
||||
bottomEnd = 24.dp
|
||||
)
|
||||
)
|
||||
.clip(inputBarShape)
|
||||
.then(
|
||||
if (theme.enableBlur) {
|
||||
Modifier
|
||||
.hazeEffect(
|
||||
state = hazeState,
|
||||
style = HazeMaterials.ultraThin()
|
||||
)
|
||||
.border(
|
||||
1.dp, MaterialTheme.colorScheme.outlineVariant,
|
||||
RoundedCornerShape(36.dp)
|
||||
style = HazeMaterials.thin()
|
||||
)
|
||||
} else Modifier
|
||||
)
|
||||
.animateContentSize()
|
||||
.weight(1f)
|
||||
.background(
|
||||
if (theme.enableBlur) Color.Transparent
|
||||
@@ -172,7 +178,9 @@ fun MessagesHistoryInputBar(
|
||||
|
||||
if (showEmojiButton) {
|
||||
Column(verticalArrangement = Arrangement.Bottom) {
|
||||
IconButton(
|
||||
RippledClickContainer(
|
||||
modifier = Modifier.size(36.dp),
|
||||
shape = CircleShape,
|
||||
onClick = {
|
||||
if (AppSettings.General.enableHaptic) {
|
||||
view.performHapticFeedback(
|
||||
@@ -196,14 +204,15 @@ fun MessagesHistoryInputBar(
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Spacer(modifier = Modifier.height(6.dp))
|
||||
}
|
||||
}
|
||||
|
||||
TextField(
|
||||
FastTextField(
|
||||
modifier = Modifier
|
||||
.focusRequester(focusRequester)
|
||||
.weight(1f)
|
||||
.heightIn(min = 48.dp)
|
||||
.appendTextContextMenuComponents {
|
||||
separator()
|
||||
|
||||
@@ -245,8 +254,11 @@ fun MessagesHistoryInputBar(
|
||||
|
||||
separator()
|
||||
},
|
||||
value = message,
|
||||
onValueChange = onMessageInputChanged,
|
||||
value = localMessage,
|
||||
onValueChange = { newValue ->
|
||||
localMessage = newValue
|
||||
onMessageInputChanged(newValue)
|
||||
},
|
||||
colors = TextFieldDefaults.colors(
|
||||
unfocusedContainerColor = Color.Transparent,
|
||||
focusedContainerColor = Color.Transparent,
|
||||
@@ -264,10 +276,12 @@ fun MessagesHistoryInputBar(
|
||||
|
||||
if (showAttachmentButton) {
|
||||
Column(verticalArrangement = Arrangement.Bottom) {
|
||||
IconButton(
|
||||
RippledClickContainer(
|
||||
modifier = Modifier.size(36.dp),
|
||||
shape = CircleShape,
|
||||
onClick = {
|
||||
onAttachmentButtonClicked()
|
||||
if (enableHaptic) {
|
||||
if (AppSettings.General.enableHaptic) {
|
||||
view.performHapticFeedback(
|
||||
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) {
|
||||
IconButton(
|
||||
RippledClickContainer(
|
||||
modifier = Modifier.size(36.dp),
|
||||
shape = CircleShape,
|
||||
onClick = {
|
||||
onActionButtonClicked()
|
||||
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))
|
||||
@@ -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,
|
||||
showEmojiButton = AppSettings.General.showEmojiButton,
|
||||
showAttachmentButton = AppSettings.General.showAttachmentButton,
|
||||
enableHaptic = AppSettings.General.enableHaptic,
|
||||
inputFieldFocusRequester = inputFieldFocusRequester,
|
||||
onBack = onBack,
|
||||
onClose = viewModel::onCloseButtonClicked,
|
||||
@@ -79,7 +78,8 @@ fun MessagesHistoryRoute(
|
||||
onUnderlineRequested = viewModel::onUnderlineClicked,
|
||||
onLinkRequested = viewModel::onLinkClicked,
|
||||
onRegularRequested = viewModel::onRegularClicked,
|
||||
onReplyCloseClicked = viewModel::onReplyCloseClicked
|
||||
onReplyCloseClicked = viewModel::onReplyCloseClicked,
|
||||
onRequestReplyToMessage = viewModel::onRequestReplyToMessage,
|
||||
)
|
||||
|
||||
HandleDialogs(
|
||||
|
||||
+7
-5
@@ -69,7 +69,6 @@ fun MessagesHistoryScreen(
|
||||
canPaginate: Boolean = false,
|
||||
showEmojiButton: Boolean = false,
|
||||
showAttachmentButton: Boolean = false,
|
||||
enableHaptic: Boolean = false,
|
||||
inputFieldFocusRequester: Boolean,
|
||||
onBack: () -> Unit = {},
|
||||
onClose: () -> Unit = {},
|
||||
@@ -94,6 +93,7 @@ fun MessagesHistoryScreen(
|
||||
onUnderlineRequested: () -> Unit = {},
|
||||
onRegularRequested: () -> Unit = {},
|
||||
onReplyCloseClicked: () -> Unit = {},
|
||||
onRequestReplyToMessage: (cmId: Long) -> Unit = {}
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val view = LocalView.current
|
||||
@@ -238,11 +238,14 @@ fun MessagesHistoryScreen(
|
||||
currentOnMessageClicked.invoke(id)
|
||||
},
|
||||
onMessageLongClicked = onMessageLongClicked,
|
||||
onPhotoClicked = onPhotoClicked
|
||||
onPhotoClicked = onPhotoClicked,
|
||||
onRequestMessageReply = onRequestReplyToMessage
|
||||
)
|
||||
|
||||
MessagesHistoryInputBar(
|
||||
modifier = Modifier.align(Alignment.BottomStart),
|
||||
InputBar(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomStart)
|
||||
.padding(bottom = 8.dp),
|
||||
message = screenState.message,
|
||||
onMessageInputChanged = onMessageInputChanged,
|
||||
onBoldRequested = onBoldRequested,
|
||||
@@ -251,7 +254,6 @@ fun MessagesHistoryScreen(
|
||||
onLinkRequested = onLinkRequested,
|
||||
onRegularRequested = onRegularRequested,
|
||||
hazeState = hazeState,
|
||||
enableHaptic = enableHaptic,
|
||||
showEmojiButton = showEmojiButton,
|
||||
showAttachmentButton = showAttachmentButton,
|
||||
actionMode = screenState.actionMode,
|
||||
|
||||
+1
-2
@@ -88,11 +88,10 @@ fun MessagesHistoryTopBarContainer(
|
||||
if (showPinnedContainer) {
|
||||
PinnedMessageContainer(
|
||||
modifier = Modifier,
|
||||
pinnedMessage = requireNotNull(pinnedMessage),
|
||||
title = pinnedTitle.orDots(),
|
||||
summary = pinnedSummary,
|
||||
canChangePin = showUnpinButton,
|
||||
onPinnedMessageClicked = onPinnedMessageClicked,
|
||||
onPinnedMessageClicked = { onPinnedMessageClicked(pinnedMessage?.id ?: -1) },
|
||||
onUnpinMessageButtonClicked = onUnpinMessageButtonClicked
|
||||
)
|
||||
HorizontalDivider()
|
||||
|
||||
+63
-5
@@ -5,8 +5,10 @@ import android.util.Log
|
||||
import android.view.HapticFeedbackConstants
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.gestures.detectHorizontalDragGestures
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
@@ -22,16 +24,23 @@ import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.view.HapticFeedbackConstantsCompat
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import dev.chrisbanes.haze.HazeState
|
||||
import dev.chrisbanes.haze.hazeSource
|
||||
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.ui.theme.LocalThemeConfig
|
||||
import dev.meloda.fast.ui.util.ImmutableList
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
@@ -58,12 +69,15 @@ fun MessagesList(
|
||||
onRequestScrollToCmId: (cmId: Long) -> Unit = {},
|
||||
onMessageClicked: (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 theme = LocalThemeConfig.current
|
||||
val view = LocalView.current
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val onAttachmentClick by rememberUpdatedState(
|
||||
{ message: UiItem.Message, attachment: VkAttachment ->
|
||||
if (isSelectedAtLeastOne) {
|
||||
@@ -137,7 +151,7 @@ fun MessagesList(
|
||||
Spacer(modifier = Modifier.height(48.dp))
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(messageBarHeight.plus(18.dp)))
|
||||
Spacer(modifier = Modifier.height(messageBarHeight.plus(12.dp)))
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.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(
|
||||
modifier = Modifier
|
||||
.then(
|
||||
@@ -199,7 +226,36 @@ fun MessagesList(
|
||||
onMessageLongClicked(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
|
||||
) {
|
||||
if (item.isOut) {
|
||||
@@ -226,7 +282,8 @@ fun MessagesList(
|
||||
if (item.replyCmId != null) {
|
||||
onRequestScrollToCmId(item.replyCmId)
|
||||
}
|
||||
}
|
||||
},
|
||||
offsetX = offsetX.value
|
||||
)
|
||||
} else {
|
||||
IncomingMessageBubble(
|
||||
@@ -252,7 +309,8 @@ fun MessagesList(
|
||||
if (item.replyCmId != null) {
|
||||
onRequestScrollToCmId(item.replyCmId)
|
||||
}
|
||||
}
|
||||
},
|
||||
offsetX = offsetX.value
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+25
@@ -2,24 +2,34 @@ package dev.meloda.fast.messageshistory.presentation
|
||||
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.offset
|
||||
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.getValue
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.conena.nanokt.android.content.dpInPx
|
||||
import dev.meloda.fast.messageshistory.model.UiItem
|
||||
import dev.meloda.fast.model.api.domain.VkAttachment
|
||||
import dev.meloda.fast.ui.R
|
||||
import dev.meloda.fast.ui.util.ImmutableList.Companion.toImmutableList
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Composable
|
||||
fun OutgoingMessageBubble(
|
||||
modifier: Modifier = Modifier,
|
||||
enableAnimations: Boolean,
|
||||
message: UiItem.Message,
|
||||
offsetX: Float = 0f,
|
||||
onClick: (VkAttachment) -> Unit = {},
|
||||
onLongClick: (VkAttachment) -> Unit = {},
|
||||
onReplyClick: () -> Unit = {}
|
||||
@@ -38,8 +48,22 @@ fun OutgoingMessageBubble(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.End
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
contentAlignment = Alignment.CenterEnd
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.round_reply_24),
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.secondary,
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterEnd)
|
||||
.offset { IntOffset(24.dpInPx + offsetX.roundToInt(), y = 0) }
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.offset { IntOffset(offsetX.roundToInt(), 0) }
|
||||
.padding(end = 16.dp)
|
||||
.fillMaxWidth(0.85f),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
@@ -65,4 +89,5 @@ fun OutgoingMessageBubble(
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+43
-20
@@ -1,15 +1,20 @@
|
||||
package dev.meloda.fast.messageshistory.presentation
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
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.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
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.res.painterResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
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
|
||||
import dev.meloda.fast.ui.components.RippledClickContainer
|
||||
|
||||
@Composable
|
||||
fun PinnedMessageContainer(
|
||||
modifier: Modifier = Modifier,
|
||||
pinnedMessage: VkMessage,
|
||||
title: String,
|
||||
summary: AnnotatedString?,
|
||||
canChangePin: Boolean,
|
||||
onPinnedMessageClicked: (Long) -> Unit = {},
|
||||
onPinnedMessageClicked: () -> Unit = {},
|
||||
onUnpinMessageButtonClicked: () -> Unit = {}
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(56.dp)
|
||||
.clickable { onPinnedMessageClicked(pinnedMessage.id) }
|
||||
.padding(start = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
.height(42.dp)
|
||||
.clickable(onClick = onPinnedMessageClicked)
|
||||
.padding(start = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_round_push_pin_24),
|
||||
contentDescription = null,
|
||||
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,
|
||||
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.Medium),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
@@ -69,6 +73,7 @@ fun PinnedMessageContainer(
|
||||
LocalContentAlpha(alpha = ContentAlpha.medium) {
|
||||
Text(
|
||||
text = summary,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
@@ -76,18 +81,36 @@ fun PinnedMessageContainer(
|
||||
}
|
||||
}
|
||||
|
||||
if (canChangePin) {
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
|
||||
IconButton(onClick = onUnpinMessageButtonClicked) {
|
||||
AnimatedVisibility(canChangePin) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
RippledClickContainer(
|
||||
modifier = Modifier.size(36.dp),
|
||||
shape = CircleShape,
|
||||
onClick = onUnpinMessageButtonClicked
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.alpha(0.5f),
|
||||
painter = painterResource(R.drawable.round_close_24px),
|
||||
contentDescription = null
|
||||
contentDescription = null,
|
||||
modifier = Modifier.alpha(0.5f),
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun PinnedMessageContainerPreview() {
|
||||
Surface {
|
||||
PinnedMessageContainer(
|
||||
title = "Иннокентий Панфилович",
|
||||
summary = buildAnnotatedString { append("Здравствуйте, как Ваше ничего?") },
|
||||
canChangePin = true,
|
||||
onPinnedMessageClicked = {},
|
||||
onUnpinMessageButtonClicked = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+11
-16
@@ -3,10 +3,8 @@ package dev.meloda.fast.messageshistory.presentation
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
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.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.surfaceColorAtElevation
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -31,25 +30,24 @@ import dev.meloda.fast.ui.components.RippledClickContainer
|
||||
|
||||
@Composable
|
||||
fun ReplyContainer(
|
||||
onCloseClicked: () -> Unit = {},
|
||||
title: String,
|
||||
text: String?,
|
||||
modifier: Modifier = Modifier,
|
||||
onCloseClicked: () -> Unit = {},
|
||||
backgroundColor: Color = MaterialTheme.colorScheme.surfaceColorAtElevation(5.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.padding(horizontal = 8.dp)
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = 48.dp)
|
||||
.clip(
|
||||
RoundedCornerShape(
|
||||
val shape = RoundedCornerShape(
|
||||
topStart = 24.dp,
|
||||
topEnd = 24.dp,
|
||||
bottomStart = 0.dp,
|
||||
bottomEnd = 0.dp
|
||||
)
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = 48.dp)
|
||||
.clip(shape)
|
||||
.background(backgroundColor)
|
||||
.padding(horizontal = 8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
@@ -99,11 +97,8 @@ fun ReplyContainer(
|
||||
@Preview
|
||||
@Composable
|
||||
private fun ReplyContainerPreview() {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.White),
|
||||
contentAlignment = Alignment.Center
|
||||
Surface(
|
||||
modifier = Modifier,
|
||||
) {
|
||||
ReplyContainer(
|
||||
onCloseClicked = {},
|
||||
|
||||
+2
-2
@@ -25,7 +25,7 @@ import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.times
|
||||
import dev.meloda.fast.ui.R
|
||||
import dev.meloda.fast.ui.components.IconButton
|
||||
import dev.meloda.fast.ui.components.FastIconButton
|
||||
import kotlin.collections.forEachIndexed
|
||||
|
||||
@Composable
|
||||
@@ -80,7 +80,7 @@ fun AudioMessage(
|
||||
}
|
||||
}
|
||||
|
||||
IconButton(
|
||||
FastIconButton(
|
||||
onClick = onPlayClick,
|
||||
modifier = Modifier
|
||||
.clip(CircleShape)
|
||||
|
||||
+2
-2
@@ -29,7 +29,7 @@ import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil.compose.AsyncImage
|
||||
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.Companion.toImmutableList
|
||||
|
||||
@@ -101,7 +101,7 @@ fun DynamicPreviewGrid(
|
||||
)
|
||||
|
||||
if (preview.isVideo) {
|
||||
IconButton(
|
||||
FastIconButton(
|
||||
onClick = { currentOnClick(index) },
|
||||
modifier = Modifier
|
||||
.size(36.dp)
|
||||
|
||||
@@ -7,7 +7,7 @@ kotlin = "2.2.21"
|
||||
ksp = "2.3.3"
|
||||
moduleGraph = "2.9.0"
|
||||
|
||||
compose-bom = "2025.11.01"
|
||||
compose-bom = "2025.12.00"
|
||||
koin = "4.1.1"
|
||||
|
||||
accompanist = "0.37.3"
|
||||
|
||||
Reference in New Issue
Block a user