diff --git a/core/ui/src/main/kotlin/dev/meloda/fast/ui/components/IconButton.kt b/core/ui/src/main/kotlin/dev/meloda/fast/ui/components/FastIconButton.kt similarity index 99% rename from core/ui/src/main/kotlin/dev/meloda/fast/ui/components/IconButton.kt rename to core/ui/src/main/kotlin/dev/meloda/fast/ui/components/FastIconButton.kt index 95e0c610..5bb829f3 100644 --- a/core/ui/src/main/kotlin/dev/meloda/fast/ui/components/IconButton.kt +++ b/core/ui/src/main/kotlin/dev/meloda/fast/ui/components/FastIconButton.kt @@ -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, diff --git a/core/ui/src/main/kotlin/dev/meloda/fast/ui/components/FastTextField.kt b/core/ui/src/main/kotlin/dev/meloda/fast/ui/components/FastTextField.kt new file mode 100644 index 00000000..0af92cef --- /dev/null +++ b/core/ui/src/main/kotlin/dev/meloda/fast/ui/components/FastTextField.kt @@ -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) + ) + }, + ) + } +} diff --git a/core/ui/src/main/kotlin/dev/meloda/fast/ui/components/RippledClickContainer.kt b/core/ui/src/main/kotlin/dev/meloda/fast/ui/components/RippledClickContainer.kt index 7cd6bb46..bb7e08c7 100644 --- a/core/ui/src/main/kotlin/dev/meloda/fast/ui/components/RippledClickContainer.kt +++ b/core/ui/src/main/kotlin/dev/meloda/fast/ui/components/RippledClickContainer.kt @@ -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 ) { diff --git a/feature/createchat/src/main/kotlin/dev/meloda/fast/conversations/presentation/CreateChatScreen.kt b/feature/createchat/src/main/kotlin/dev/meloda/fast/conversations/presentation/CreateChatScreen.kt index 931dc713..7b6ae2c6 100644 --- a/feature/createchat/src/main/kotlin/dev/meloda/fast/conversations/presentation/CreateChatScreen.kt +++ b/feature/createchat/src/main/kotlin/dev/meloda/fast/conversations/presentation/CreateChatScreen.kt @@ -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 diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryViewModel.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryViewModel.kt index 4f08f01f..2d3813a8 100644 --- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryViewModel.kt +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryViewModel.kt @@ -63,4 +63,6 @@ interface MessagesHistoryViewModel { fun onRegularClicked() fun onReplyCloseClicked() + + fun onRequestReplyToMessage(cmId: Long) } diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryViewModelImpl.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryViewModelImpl.kt index 1d6a61eb..c30b5181 100644 --- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryViewModelImpl.kt +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryViewModelImpl.kt @@ -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,12 +333,10 @@ 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)) + ) } } @@ -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 diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/IncomingMessageBubble.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/IncomingMessageBubble.kt index 62adcd91..7954defd 100644 --- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/IncomingMessageBubble.kt +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/IncomingMessageBubble.kt @@ -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,66 +61,79 @@ fun IncomingMessageBubble( else Modifier ), ) { - Row( - modifier = Modifier - .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 - .padding(bottom = 6.dp) - .size(28.dp) - .alpha(if (message.showAvatar) 1f else 0f) - .clip(CircleShape), - ) - Spacer(modifier = Modifier.width(8.dp)) - } + 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) } + ) - Column { - AnimatedVisibility(visible = message.showName) { - Text( + Row( + modifier = Modifier + .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 - .padding(start = 12.dp) - .widthIn(max = 140.dp), - text = message.name, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.primary, - maxLines = 1, - overflow = TextOverflow.Ellipsis, + .padding(bottom = 6.dp) + .size(28.dp) + .alpha(if (message.showAvatar) 1f else 0f) + .clip(CircleShape), ) + Spacer(modifier = Modifier.width(8.dp)) } - MessageBubble( - modifier = Modifier, - 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 - ) + Column { + AnimatedVisibility(visible = message.showName) { + Text( + modifier = Modifier + .padding(start = 12.dp) + .widthIn(max = 140.dp), + text = message.name, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.primary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + + MessageBubble( + modifier = Modifier, + 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)) } } diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesHistoryInputBar.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/InputBar.kt similarity index 82% rename from feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesHistoryInputBar.kt rename to feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/InputBar.kt index ff8fe667..d778c708 100644 --- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesHistoryInputBar.kt +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/InputBar.kt @@ -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 + ) +} diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesHistoryRoute.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesHistoryRoute.kt index 0f396816..10224e81 100644 --- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesHistoryRoute.kt +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesHistoryRoute.kt @@ -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( diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesHistoryScreen.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesHistoryScreen.kt index f999ed56..042e34aa 100644 --- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesHistoryScreen.kt +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesHistoryScreen.kt @@ -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, diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesHistoryTopBarContainer.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesHistoryTopBarContainer.kt index 75a51d18..b0e78a54 100644 --- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesHistoryTopBarContainer.kt +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesHistoryTopBarContainer.kt @@ -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() diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesList.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesList.kt index 0e2f7226..979e0d4e 100644 --- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesList.kt +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesList.kt @@ -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, index: Int) -> Unit = { _, _ -> } + onPhotoClicked: (images: List, 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 ) } } diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/OutgoingMessageBubble.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/OutgoingMessageBubble.kt index c8f4e953..7ca744e5 100644 --- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/OutgoingMessageBubble.kt +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/OutgoingMessageBubble.kt @@ -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,31 +48,46 @@ fun OutgoingMessageBubble( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.End ) { - Row( - modifier = Modifier - .padding(end = 16.dp) - .fillMaxWidth(0.85f), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.End + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.CenterEnd ) { - 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 + 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, + 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 + ) + } } } } diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/PinnedMessageContainer.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/PinnedMessageContainer.kt index ded147dd..9206e840 100644 --- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/PinnedMessageContainer.kt +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/PinnedMessageContainer.kt @@ -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)) + AnimatedVisibility(canChangePin) { + 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) { - Icon( - modifier = Modifier.alpha(0.5f), - painter = painterResource(R.drawable.round_close_24px), - contentDescription = null - ) + Spacer(modifier = Modifier.width(4.dp)) } - - Spacer(modifier = Modifier.width(8.dp)) } } } + +@Preview +@Composable +private fun PinnedMessageContainerPreview() { + Surface { + PinnedMessageContainer( + title = "Иннокентий Панфилович", + summary = buildAnnotatedString { append("Здравствуйте, как Ваше ничего?") }, + canChangePin = true, + onPinnedMessageClicked = {}, + onUnpinMessageButtonClicked = {} + ) + } +} diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/ReplyContainer.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/ReplyContainer.kt index 11455186..e5aded22 100644 --- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/ReplyContainer.kt +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/ReplyContainer.kt @@ -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) ) { + val shape = RoundedCornerShape( + topStart = 24.dp, + topEnd = 24.dp, + bottomStart = 0.dp, + bottomEnd = 0.dp + ) + Row( modifier = modifier - .padding(horizontal = 8.dp) .fillMaxWidth() .heightIn(min = 48.dp) - .clip( - RoundedCornerShape( - topStart = 24.dp, - topEnd = 24.dp, - bottomStart = 0.dp, - bottomEnd = 0.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 = {}, diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/attachments/AudioMessage.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/attachments/AudioMessage.kt index 3502f1a6..a6fb3983 100644 --- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/attachments/AudioMessage.kt +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/attachments/AudioMessage.kt @@ -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) diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/attachments/Previews.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/attachments/Previews.kt index adf3673b..e6477db7 100644 --- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/attachments/Previews.kt +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/attachments/Previews.kt @@ -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) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 32cded39..14f1e4ab 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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"