Compare commits
7 Commits
018151ad18
...
c666bd46f3
| Author | SHA1 | Date | |
|---|---|---|---|
| c666bd46f3 | |||
| 421ca27758 | |||
| 8231062ca5 | |||
| 723555f634 | |||
| 3e05744a18 | |||
| 821ee46cef | |||
| dcddddea9b |
@@ -3,6 +3,9 @@ name: Android CI Build
|
|||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
env:
|
env:
|
||||||
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
|
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
|
||||||
RELEASE_SIGN_KEY_ALIAS: ${{ secrets.RELEASE_SIGN_KEY_ALIAS }}
|
RELEASE_SIGN_KEY_ALIAS: ${{ secrets.RELEASE_SIGN_KEY_ALIAS }}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
name: Android CI Release
|
name: Android CI Release
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ Unofficial messenger for russian social network VKontakte
|
|||||||
- [x] Send messages
|
- [x] Send messages
|
||||||
- [x] Pinned message
|
- [x] Pinned message
|
||||||
- [x] Pin & unpin messages
|
- [x] Pin & unpin messages
|
||||||
- [ ] Reply to message
|
- [x] Reply to message
|
||||||
- [x] Delete message
|
- [x] Delete message
|
||||||
- [x] Select multiple messages
|
- [x] Select multiple messages
|
||||||
- [x] Delete
|
- [x] Delete
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package dev.meloda.fast.common.util
|
||||||
|
|
||||||
|
import java.net.URLEncoder
|
||||||
|
|
||||||
|
fun String.urlEncode(encoding: String = "utf-8"): String {
|
||||||
|
return URLEncoder.encode(this, encoding)
|
||||||
|
}
|
||||||
@@ -32,7 +32,7 @@ interface MessagesRepository {
|
|||||||
peerId: Long,
|
peerId: Long,
|
||||||
randomId: Long,
|
randomId: Long,
|
||||||
message: String?,
|
message: String?,
|
||||||
replyTo: Long?,
|
forward: String?,
|
||||||
attachments: List<VkAttachment>?,
|
attachments: List<VkAttachment>?,
|
||||||
formatData: VkMessage.FormatData?
|
formatData: VkMessage.FormatData?
|
||||||
): ApiResult<MessagesSendResponse, RestApiErrorDomain>
|
): ApiResult<MessagesSendResponse, RestApiErrorDomain>
|
||||||
|
|||||||
+2
-2
@@ -195,7 +195,7 @@ class MessagesRepositoryImpl(
|
|||||||
peerId: Long,
|
peerId: Long,
|
||||||
randomId: Long,
|
randomId: Long,
|
||||||
message: String?,
|
message: String?,
|
||||||
replyTo: Long?,
|
forward: String?,
|
||||||
attachments: List<VkAttachment>?,
|
attachments: List<VkAttachment>?,
|
||||||
formatData: VkMessage.FormatData?
|
formatData: VkMessage.FormatData?
|
||||||
): ApiResult<MessagesSendResponse, RestApiErrorDomain> = withContext(Dispatchers.IO) {
|
): ApiResult<MessagesSendResponse, RestApiErrorDomain> = withContext(Dispatchers.IO) {
|
||||||
@@ -203,7 +203,7 @@ class MessagesRepositoryImpl(
|
|||||||
peerId = peerId,
|
peerId = peerId,
|
||||||
randomId = randomId,
|
randomId = randomId,
|
||||||
message = message,
|
message = message,
|
||||||
replyTo = replyTo,
|
forward = forward,
|
||||||
attachments = attachments,
|
attachments = attachments,
|
||||||
formatData = formatData
|
formatData = formatData
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ interface MessagesUseCase : BaseUseCase {
|
|||||||
peerId: Long,
|
peerId: Long,
|
||||||
randomId: Long,
|
randomId: Long,
|
||||||
message: String?,
|
message: String?,
|
||||||
replyTo: Long?,
|
forward: String?,
|
||||||
attachments: List<VkAttachment>?,
|
attachments: List<VkAttachment>?,
|
||||||
formatData: VkMessage.FormatData?
|
formatData: VkMessage.FormatData?
|
||||||
): Flow<State<MessagesSendResponse>>
|
): Flow<State<MessagesSendResponse>>
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ class MessagesUseCaseImpl(
|
|||||||
peerId: Long,
|
peerId: Long,
|
||||||
randomId: Long,
|
randomId: Long,
|
||||||
message: String?,
|
message: String?,
|
||||||
replyTo: Long?,
|
forward: String?,
|
||||||
attachments: List<VkAttachment>?,
|
attachments: List<VkAttachment>?,
|
||||||
formatData: VkMessage.FormatData?
|
formatData: VkMessage.FormatData?
|
||||||
): Flow<State<MessagesSendResponse>> = flowNewState {
|
): Flow<State<MessagesSendResponse>> = flowNewState {
|
||||||
@@ -64,7 +64,7 @@ class MessagesUseCaseImpl(
|
|||||||
peerId = peerId,
|
peerId = peerId,
|
||||||
randomId = randomId,
|
randomId = randomId,
|
||||||
message = message,
|
message = message,
|
||||||
replyTo = replyTo,
|
forward = forward,
|
||||||
attachments = attachments,
|
attachments = attachments,
|
||||||
formatData = formatData
|
formatData = formatData
|
||||||
).mapToState()
|
).mapToState()
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ data class MessagesSendRequest(
|
|||||||
val message: String?,
|
val message: String?,
|
||||||
val lat: Int? = null,
|
val lat: Int? = null,
|
||||||
val lon: Int? = null,
|
val lon: Int? = null,
|
||||||
val replyTo: Long? = null,
|
val forward: String? = null,
|
||||||
val stickerId: Long? = null,
|
val stickerId: Long? = null,
|
||||||
val disableMentions: Boolean? = null,
|
val disableMentions: Boolean? = null,
|
||||||
val doNotParseLinks: Boolean? = null,
|
val doNotParseLinks: Boolean? = null,
|
||||||
@@ -51,7 +51,7 @@ data class MessagesSendRequest(
|
|||||||
message?.let { this["message"] = it }
|
message?.let { this["message"] = it }
|
||||||
lat?.let { this["lat"] = it.toString() }
|
lat?.let { this["lat"] = it.toString() }
|
||||||
lon?.let { this["lon"] = it.toString() }
|
lon?.let { this["lon"] = it.toString() }
|
||||||
replyTo?.let { this["reply_to"] = it.toString() }
|
forward?.let { this["forward"] = it }
|
||||||
stickerId?.let { this["sticker_id"] = it.toString() }
|
stickerId?.let { this["sticker_id"] = it.toString() }
|
||||||
disableMentions?.let { this["disable_mentions"] = it.asInt().toString() }
|
disableMentions?.let { this["disable_mentions"] = it.asInt().toString() }
|
||||||
doNotParseLinks?.let { this["dont_parse_links"] = it.asInt().toString() }
|
doNotParseLinks?.let { this["dont_parse_links"] = it.asInt().toString() }
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package dev.meloda.fast.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.ripple
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Shape
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RippledClickContainer(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
shape: Shape = RoundedCornerShape(4.dp),
|
||||||
|
onClick: () -> Unit,
|
||||||
|
content: @Composable () -> Unit
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.clip(shape)
|
||||||
|
.clickable(
|
||||||
|
interactionSource = null,
|
||||||
|
indication = ripple(),
|
||||||
|
onClick = onClick
|
||||||
|
),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:autoMirrored="true"
|
||||||
|
android:viewportWidth="960"
|
||||||
|
android:viewportHeight="960">
|
||||||
|
<path
|
||||||
|
android:fillColor="#ffffff"
|
||||||
|
android:pathData="M760,760L760,600Q760,550 725,515Q690,480 640,480L273,480L417,624L360,680L120,440L360,200L417,256L273,400L640,400Q723,400 781.5,458.5Q840,517 840,600L840,760L760,760Z" />
|
||||||
|
</vector>
|
||||||
+4
-1491
File diff suppressed because it is too large
Load Diff
+1563
File diff suppressed because it is too large
Load Diff
+6
-2
@@ -24,7 +24,9 @@ data class MessagesHistoryScreenState(
|
|||||||
val conversation: VkConversation,
|
val conversation: VkConversation,
|
||||||
val pinnedMessage: VkMessage?,
|
val pinnedMessage: VkMessage?,
|
||||||
val pinnedTitle: String?,
|
val pinnedTitle: String?,
|
||||||
val pinnedSummary: AnnotatedString?
|
val pinnedSummary: AnnotatedString?,
|
||||||
|
val replyTitle: String?,
|
||||||
|
val replyText: String?
|
||||||
) {
|
) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -43,7 +45,9 @@ data class MessagesHistoryScreenState(
|
|||||||
conversation = VkConversation.EMPTY,
|
conversation = VkConversation.EMPTY,
|
||||||
pinnedMessage = null,
|
pinnedMessage = null,
|
||||||
pinnedTitle = null,
|
pinnedTitle = null,
|
||||||
pinnedSummary = null
|
pinnedSummary = null,
|
||||||
|
replyTitle = null,
|
||||||
|
replyText = null,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-2
@@ -117,7 +117,7 @@ fun MessageOptionsDialog(
|
|||||||
options += if (message.isPinned) MessageOption.Unpin else MessageOption.Pin
|
options += if (message.isPinned) MessageOption.Unpin else MessageOption.Pin
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!message.isRead(screenState.conversation)) {
|
if (!message.isOut && !message.isRead(screenState.conversation)) {
|
||||||
options += MessageOption.Read
|
options += MessageOption.Read
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,7 +180,13 @@ fun MessageOptionsDialog(
|
|||||||
onClick = {
|
onClick = {
|
||||||
onDismissed()
|
onDismissed()
|
||||||
val pickedOption = options[index]
|
val pickedOption = options[index]
|
||||||
onItemPicked(bundleOf("option" to pickedOption))
|
onItemPicked(
|
||||||
|
bundleOf(
|
||||||
|
"option" to pickedOption,
|
||||||
|
"messageId" to message.id,
|
||||||
|
"cmId" to message.cmId
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+44
-7
@@ -1,7 +1,9 @@
|
|||||||
package dev.meloda.fast.messageshistory.presentation
|
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.animateContentSize
|
import androidx.compose.animation.animateContentSize
|
||||||
|
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
|
||||||
import androidx.compose.animation.scaleIn
|
import androidx.compose.animation.scaleIn
|
||||||
@@ -32,10 +34,14 @@ 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.rememberCoroutineScope
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
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
|
||||||
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.layout.onGloballyPositioned
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
@@ -54,9 +60,9 @@ import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
|||||||
import dev.chrisbanes.haze.materials.HazeMaterials
|
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.components.IconButton
|
import dev.meloda.fast.ui.components.IconButton
|
||||||
import dev.meloda.fast.ui.theme.LocalThemeConfig
|
import dev.meloda.fast.ui.theme.LocalThemeConfig
|
||||||
import dev.meloda.fast.ui.R
|
|
||||||
|
|
||||||
@OptIn(ExperimentalLayoutApi::class, ExperimentalHazeMaterialsApi::class)
|
@OptIn(ExperimentalLayoutApi::class, ExperimentalHazeMaterialsApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -68,6 +74,9 @@ fun MessagesHistoryInputBar(
|
|||||||
showEmojiButton: Boolean,
|
showEmojiButton: Boolean,
|
||||||
showAttachmentButton: Boolean,
|
showAttachmentButton: Boolean,
|
||||||
actionMode: ActionMode,
|
actionMode: ActionMode,
|
||||||
|
replyTitle: String?,
|
||||||
|
replyText: String?,
|
||||||
|
inputFieldFocusRequester: Boolean,
|
||||||
onMessageInputChanged: (TextFieldValue) -> Unit = {},
|
onMessageInputChanged: (TextFieldValue) -> Unit = {},
|
||||||
onBoldRequested: () -> Unit = {},
|
onBoldRequested: () -> Unit = {},
|
||||||
onItalicRequested: () -> Unit = {},
|
onItalicRequested: () -> Unit = {},
|
||||||
@@ -77,16 +86,28 @@ fun MessagesHistoryInputBar(
|
|||||||
onSetMessageBarHeight: (Dp) -> Unit = {},
|
onSetMessageBarHeight: (Dp) -> Unit = {},
|
||||||
onEmojiButtonLongClicked: () -> Unit = {},
|
onEmojiButtonLongClicked: () -> Unit = {},
|
||||||
onAttachmentButtonClicked: () -> Unit = {},
|
onAttachmentButtonClicked: () -> Unit = {},
|
||||||
onActionButtonClicked: () -> Unit = {}
|
onActionButtonClicked: () -> Unit = {},
|
||||||
|
onReplyCloseClicked: () -> Unit = {}
|
||||||
) {
|
) {
|
||||||
val view = LocalView.current
|
val view = LocalView.current
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
|
|
||||||
val scope = rememberCoroutineScope()
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
|
||||||
|
LaunchedEffect(inputFieldFocusRequester) {
|
||||||
|
if (inputFieldFocusRequester) {
|
||||||
|
focusRequester.requestFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val theme = LocalThemeConfig.current
|
val theme = LocalThemeConfig.current
|
||||||
|
|
||||||
|
val inputBarTopCornerRadius by animateDpAsState(
|
||||||
|
targetValue = if (replyTitle == null) 24.dp else 0.dp,
|
||||||
|
label = "inputBarTopCornerRadius"
|
||||||
|
)
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -95,6 +116,14 @@ fun MessagesHistoryInputBar(
|
|||||||
.navigationBarsPadding()
|
.navigationBarsPadding()
|
||||||
.imePadding()
|
.imePadding()
|
||||||
) {
|
) {
|
||||||
|
AnimatedVisibility(replyTitle != null) {
|
||||||
|
ReplyContainer(
|
||||||
|
title = replyTitle.orEmpty(),
|
||||||
|
text = replyText.orEmpty(),
|
||||||
|
onCloseClicked = onReplyCloseClicked,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -102,10 +131,17 @@ fun MessagesHistoryInputBar(
|
|||||||
.imeNestedScroll(),
|
.imeNestedScroll(),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Spacer(modifier = Modifier.width(10.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clip(RoundedCornerShape(36.dp))
|
.clip(
|
||||||
|
RoundedCornerShape(
|
||||||
|
topStart = inputBarTopCornerRadius,
|
||||||
|
topEnd = inputBarTopCornerRadius,
|
||||||
|
bottomStart = 24.dp,
|
||||||
|
bottomEnd = 24.dp
|
||||||
|
)
|
||||||
|
)
|
||||||
.then(
|
.then(
|
||||||
if (theme.enableBlur) {
|
if (theme.enableBlur) {
|
||||||
Modifier
|
Modifier
|
||||||
@@ -166,6 +202,7 @@ fun MessagesHistoryInputBar(
|
|||||||
|
|
||||||
TextField(
|
TextField(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
.focusRequester(focusRequester)
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.appendTextContextMenuComponents {
|
.appendTextContextMenuComponents {
|
||||||
separator()
|
separator()
|
||||||
@@ -287,7 +324,7 @@ fun MessagesHistoryInputBar(
|
|||||||
Spacer(modifier = Modifier.width(6.dp))
|
Spacer(modifier = Modifier.width(6.dp))
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.width(10.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-2
@@ -4,7 +4,6 @@ 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.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import dev.meloda.fast.common.model.UiImage
|
|
||||||
import dev.meloda.fast.datastore.AppSettings
|
import dev.meloda.fast.datastore.AppSettings
|
||||||
import dev.meloda.fast.messageshistory.MessagesHistoryViewModel
|
import dev.meloda.fast.messageshistory.MessagesHistoryViewModel
|
||||||
import dev.meloda.fast.messageshistory.MessagesHistoryViewModelImpl
|
import dev.meloda.fast.messageshistory.MessagesHistoryViewModelImpl
|
||||||
@@ -30,6 +29,7 @@ fun MessagesHistoryRoute(
|
|||||||
val baseError by viewModel.baseError.collectAsStateWithLifecycle()
|
val baseError by viewModel.baseError.collectAsStateWithLifecycle()
|
||||||
val canPaginate by viewModel.canPaginate.collectAsStateWithLifecycle()
|
val canPaginate by viewModel.canPaginate.collectAsStateWithLifecycle()
|
||||||
val scrollIndex by viewModel.isNeedToScrollToIndex.collectAsStateWithLifecycle()
|
val scrollIndex by viewModel.isNeedToScrollToIndex.collectAsStateWithLifecycle()
|
||||||
|
val inputFieldFocusRequester by viewModel.inputFieldFocusRequester.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
LaunchedEffect(navigationEvent) {
|
LaunchedEffect(navigationEvent) {
|
||||||
val needToConsume = when (val navigation = navigationEvent) {
|
val needToConsume = when (val navigation = navigationEvent) {
|
||||||
@@ -56,6 +56,7 @@ fun MessagesHistoryRoute(
|
|||||||
showEmojiButton = AppSettings.General.showEmojiButton,
|
showEmojiButton = AppSettings.General.showEmojiButton,
|
||||||
showAttachmentButton = AppSettings.General.showAttachmentButton,
|
showAttachmentButton = AppSettings.General.showAttachmentButton,
|
||||||
enableHaptic = AppSettings.General.enableHaptic,
|
enableHaptic = AppSettings.General.enableHaptic,
|
||||||
|
inputFieldFocusRequester = inputFieldFocusRequester,
|
||||||
onBack = onBack,
|
onBack = onBack,
|
||||||
onClose = viewModel::onCloseButtonClicked,
|
onClose = viewModel::onCloseButtonClicked,
|
||||||
onScrolledToIndex = viewModel::onScrolledToIndex,
|
onScrolledToIndex = viewModel::onScrolledToIndex,
|
||||||
@@ -77,7 +78,8 @@ fun MessagesHistoryRoute(
|
|||||||
onItalicRequested = viewModel::onItalicClicked,
|
onItalicRequested = viewModel::onItalicClicked,
|
||||||
onUnderlineRequested = viewModel::onUnderlineClicked,
|
onUnderlineRequested = viewModel::onUnderlineClicked,
|
||||||
onLinkRequested = viewModel::onLinkClicked,
|
onLinkRequested = viewModel::onLinkClicked,
|
||||||
onRegularRequested = viewModel::onRegularClicked
|
onRegularRequested = viewModel::onRegularClicked,
|
||||||
|
onReplyCloseClicked = viewModel::onReplyCloseClicked
|
||||||
)
|
)
|
||||||
|
|
||||||
HandleDialogs(
|
HandleDialogs(
|
||||||
|
|||||||
+10
-3
@@ -44,13 +44,13 @@ import dev.meloda.fast.messageshistory.model.UiItem
|
|||||||
import dev.meloda.fast.messageshistory.util.indexOfMessageByCmId
|
import dev.meloda.fast.messageshistory.util.indexOfMessageByCmId
|
||||||
import dev.meloda.fast.model.BaseError
|
import dev.meloda.fast.model.BaseError
|
||||||
import dev.meloda.fast.model.api.domain.VkMessage
|
import dev.meloda.fast.model.api.domain.VkMessage
|
||||||
|
import dev.meloda.fast.ui.R
|
||||||
import dev.meloda.fast.ui.components.Loader
|
import dev.meloda.fast.ui.components.Loader
|
||||||
import dev.meloda.fast.ui.components.VkErrorView
|
import dev.meloda.fast.ui.components.VkErrorView
|
||||||
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 dev.meloda.fast.ui.util.emptyImmutableList
|
import dev.meloda.fast.ui.util.emptyImmutableList
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import dev.meloda.fast.ui.R
|
|
||||||
|
|
||||||
@OptIn(
|
@OptIn(
|
||||||
ExperimentalMaterial3Api::class,
|
ExperimentalMaterial3Api::class,
|
||||||
@@ -70,6 +70,7 @@ fun MessagesHistoryScreen(
|
|||||||
showEmojiButton: Boolean = false,
|
showEmojiButton: Boolean = false,
|
||||||
showAttachmentButton: Boolean = false,
|
showAttachmentButton: Boolean = false,
|
||||||
enableHaptic: Boolean = false,
|
enableHaptic: Boolean = false,
|
||||||
|
inputFieldFocusRequester: Boolean,
|
||||||
onBack: () -> Unit = {},
|
onBack: () -> Unit = {},
|
||||||
onClose: () -> Unit = {},
|
onClose: () -> Unit = {},
|
||||||
onScrolledToIndex: () -> Unit = {},
|
onScrolledToIndex: () -> Unit = {},
|
||||||
@@ -91,7 +92,8 @@ fun MessagesHistoryScreen(
|
|||||||
onItalicRequested: () -> Unit = {},
|
onItalicRequested: () -> Unit = {},
|
||||||
onLinkRequested: () -> Unit = {},
|
onLinkRequested: () -> Unit = {},
|
||||||
onUnderlineRequested: () -> Unit = {},
|
onUnderlineRequested: () -> Unit = {},
|
||||||
onRegularRequested: () -> Unit = {}
|
onRegularRequested: () -> Unit = {},
|
||||||
|
onReplyCloseClicked: () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val view = LocalView.current
|
val view = LocalView.current
|
||||||
@@ -215,6 +217,7 @@ fun MessagesHistoryScreen(
|
|||||||
uiMessages = uiMessages,
|
uiMessages = uiMessages,
|
||||||
isSelectedAtLeastOne = isSelectedAtLeastOne,
|
isSelectedAtLeastOne = isSelectedAtLeastOne,
|
||||||
isPaginating = screenState.isPaginating,
|
isPaginating = screenState.isPaginating,
|
||||||
|
isReplying = screenState.replyTitle != null,
|
||||||
messageBarHeight = messageBarHeight,
|
messageBarHeight = messageBarHeight,
|
||||||
onRequestScrollToCmId = { cmId ->
|
onRequestScrollToCmId = { cmId ->
|
||||||
val index = uiMessages.values.indexOfMessageByCmId(cmId)
|
val index = uiMessages.values.indexOfMessageByCmId(cmId)
|
||||||
@@ -252,10 +255,14 @@ fun MessagesHistoryScreen(
|
|||||||
showEmojiButton = showEmojiButton,
|
showEmojiButton = showEmojiButton,
|
||||||
showAttachmentButton = showAttachmentButton,
|
showAttachmentButton = showAttachmentButton,
|
||||||
actionMode = screenState.actionMode,
|
actionMode = screenState.actionMode,
|
||||||
|
replyTitle = screenState.replyTitle,
|
||||||
|
replyText = screenState.replyText,
|
||||||
|
inputFieldFocusRequester = inputFieldFocusRequester,
|
||||||
onSetMessageBarHeight = { messageBarHeight = it },
|
onSetMessageBarHeight = { messageBarHeight = it },
|
||||||
onEmojiButtonLongClicked = onEmojiButtonLongClicked,
|
onEmojiButtonLongClicked = onEmojiButtonLongClicked,
|
||||||
onAttachmentButtonClicked = onAttachmentButtonClicked,
|
onAttachmentButtonClicked = onAttachmentButtonClicked,
|
||||||
onActionButtonClicked = onActionButtonClicked
|
onActionButtonClicked = onActionButtonClicked,
|
||||||
|
onReplyCloseClicked = onReplyCloseClicked
|
||||||
)
|
)
|
||||||
|
|
||||||
when {
|
when {
|
||||||
|
|||||||
+5
@@ -53,6 +53,7 @@ fun MessagesList(
|
|||||||
uiMessages: ImmutableList<UiItem>,
|
uiMessages: ImmutableList<UiItem>,
|
||||||
isSelectedAtLeastOne: Boolean,
|
isSelectedAtLeastOne: Boolean,
|
||||||
isPaginating: Boolean,
|
isPaginating: Boolean,
|
||||||
|
isReplying: Boolean,
|
||||||
messageBarHeight: Dp,
|
messageBarHeight: Dp,
|
||||||
onRequestScrollToCmId: (cmId: Long) -> Unit = {},
|
onRequestScrollToCmId: (cmId: Long) -> Unit = {},
|
||||||
onMessageClicked: (Long) -> Unit = {},
|
onMessageClicked: (Long) -> Unit = {},
|
||||||
@@ -132,6 +133,10 @@ fun MessagesList(
|
|||||||
reverseLayout = true
|
reverseLayout = true
|
||||||
) {
|
) {
|
||||||
item {
|
item {
|
||||||
|
AnimatedVisibility(isReplying) {
|
||||||
|
Spacer(modifier = Modifier.height(48.dp))
|
||||||
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(messageBarHeight.plus(18.dp)))
|
Spacer(modifier = Modifier.height(messageBarHeight.plus(18.dp)))
|
||||||
Spacer(
|
Spacer(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|||||||
+114
@@ -0,0 +1,114 @@
|
|||||||
|
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
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
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.Text
|
||||||
|
import androidx.compose.material3.surfaceColorAtElevation
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import dev.meloda.fast.ui.R
|
||||||
|
import dev.meloda.fast.ui.components.RippledClickContainer
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ReplyContainer(
|
||||||
|
onCloseClicked: () -> Unit = {},
|
||||||
|
title: String,
|
||||||
|
text: String?,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
backgroundColor: Color = MaterialTheme.colorScheme.surfaceColorAtElevation(5.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
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.background(backgroundColor)
|
||||||
|
.padding(horizontal = 8.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.round_reply_24px),
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(0.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = MaterialTheme.typography.labelLarge,
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
|
||||||
|
AnimatedVisibility(text != null) {
|
||||||
|
Text(
|
||||||
|
text = text.orEmpty(),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RippledClickContainer(
|
||||||
|
modifier = Modifier.size(36.dp),
|
||||||
|
shape = CircleShape,
|
||||||
|
onClick = onCloseClicked
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.round_close_24px),
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
private fun ReplyContainerPreview() {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Color.White),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
ReplyContainer(
|
||||||
|
onCloseClicked = {},
|
||||||
|
title = "В ответ Ишак",
|
||||||
|
text = "Приветствую тебя, Ишак!",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user