android: render reply quotes and support message long press
This commit is contained in:
1 parent
d28b7062d2
commit
30102f5f42
3 files changed
+82
-5
No files matched your search
+10
-4
@@ -140,7 +140,7 @@ fun MessageBubble(message: Message, playbackState: ChatViewModel.PlaybackState =
|
||||
onOpenImage: (Message) -> Unit = {},
|
||||
onPlayVideo: (Message) -> Unit = {},
|
||||
onLink: (String) -> Unit = {},
|
||||
footer: @Composable () -> Unit = {}) {
|
||||
footer: @Composable () -> Unit = {}, header: @Composable () -> Unit = {}) {
|
||||
val bgColor = if (message.isOutgoing) ChatColors.OutgoingBubble else ChatColors.IncomingBubble
|
||||
val textColor = ChatColors.Text
|
||||
val alignment = if (message.isOutgoing) Arrangement.End else Arrangement.Start
|
||||
@@ -154,7 +154,7 @@ fun MessageBubble(message: Message, playbackState: ChatViewModel.PlaybackState =
|
||||
val isActive = isVoice && playbackState.filePath == message.filePath
|
||||
|
||||
val emojiCount = if (isVoice || isVideo || isImage || isMedia) 0 else detectEmojiOnly(message.text)
|
||||
if (emojiCount > 0) {
|
||||
if (emojiCount > 0 && message.replyTo.isEmpty()) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 2.dp),
|
||||
horizontalArrangement = alignment
|
||||
@@ -187,6 +187,7 @@ fun MessageBubble(message: Message, playbackState: ChatViewModel.PlaybackState =
|
||||
.padding(12.dp)
|
||||
) {
|
||||
Column {
|
||||
header()
|
||||
if (!isVoice) Text(if (message.isOutgoing) stringResource(R.string.you) else message.author,
|
||||
fontSize = 13.sp, fontWeight = FontWeight.Bold, color = ChatColors.Secondary)
|
||||
Spacer(Modifier.height(2.dp))
|
||||
@@ -582,6 +583,7 @@ fun InputBar(
|
||||
isVideoMode: Boolean = false,
|
||||
onToggleMode: () -> Unit = {},
|
||||
onVideoRecordOpen: () -> Unit = {},
|
||||
focusRevision: Long = 0,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val ctx = LocalContext.current
|
||||
@@ -589,6 +591,9 @@ fun InputBar(
|
||||
val focusManager = LocalFocusManager.current
|
||||
val keyboard = LocalSoftwareKeyboardController.current
|
||||
val inputFocus = remember { FocusRequester() }
|
||||
LaunchedEffect(focusRevision) {
|
||||
if (focusRevision > 0 && enabled && !isRecording) { inputFocus.requestFocus(); keyboard?.show() }
|
||||
}
|
||||
var emojiPanel by remember { mutableStateOf(false) }
|
||||
fun closeEmojiPanel(reason: String) {
|
||||
if (!emojiPanel) return
|
||||
@@ -742,7 +747,7 @@ fun InputBar(
|
||||
modifier = Modifier
|
||||
.size(44.dp)
|
||||
.background(sendColor, CircleShape)
|
||||
.clickable { onSend() },
|
||||
.clickable(enabled = enabled) { onSend() },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text("\u27A4", fontSize = 22.sp, color = MaterialTheme.colorScheme.onPrimary)
|
||||
@@ -754,7 +759,8 @@ fun InputBar(
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.background(pttBg, RoundedCornerShape(4.dp))
|
||||
.pointerInput(isVideoMode, hasMicPerm) {
|
||||
.pointerInput(isVideoMode, hasMicPerm, enabled) {
|
||||
if (!enabled) return@pointerInput
|
||||
awaitPointerEventScope {
|
||||
while (true) {
|
||||
val down = awaitFirstDown(requireUnconsumed = false)
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
package com.utun.chat.ui.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.gestures.awaitEachGesture
|
||||
import androidx.compose.foundation.gestures.awaitFirstDown
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.pointer.PointerEventPass
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.onLongClick
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.utun.chat.R
|
||||
import com.utun.chat.data.Message
|
||||
|
||||
/** Initial-pass позволяет открыть меню поверх ссылок, плеера и кнопок голосования. */
|
||||
@Composable
|
||||
internal fun Modifier.messageLongPress(label: String, onLongPress: () -> Unit): Modifier {
|
||||
val action by rememberUpdatedState(onLongPress)
|
||||
return semantics { onLongClick(label) { action(); true } }.pointerInput(Unit) {
|
||||
awaitEachGesture {
|
||||
val down = awaitFirstDown(requireUnconsumed = false, pass = PointerEventPass.Initial)
|
||||
val releasedOrMoved = withTimeoutOrNull(viewConfiguration.longPressTimeoutMillis) {
|
||||
do {
|
||||
val event = awaitPointerEvent(PointerEventPass.Initial)
|
||||
val change = event.changes.firstOrNull { it.id == down.id } ?: return@withTimeoutOrNull true
|
||||
if (!change.pressed || (change.position - down.position).getDistance() > viewConfiguration.touchSlop)
|
||||
return@withTimeoutOrNull true
|
||||
} while (true)
|
||||
}
|
||||
if (releasedOrMoved == null) {
|
||||
action()
|
||||
do {
|
||||
val event = awaitPointerEvent(PointerEventPass.Initial)
|
||||
event.changes.forEach { it.consume() }
|
||||
} while (event.changes.any { it.pressed })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ReplyQuote(original: Message?, modifier: Modifier = Modifier, onClick: (() -> Unit)? = null) {
|
||||
val preview = when {
|
||||
original == null -> stringResource(R.string.reply_original_missing)
|
||||
original.contentType == "audio/opus" -> stringResource(R.string.reply_voice)
|
||||
original.contentType.startsWith("image/") -> stringResource(R.string.photo)
|
||||
original.contentType.startsWith("video/") -> stringResource(R.string.video_message)
|
||||
original.contentType == "poll" -> original.feedback?.poll?.question ?: stringResource(R.string.reply_poll)
|
||||
else -> original.text.replace(Regex("\\s+"), " ").take(160)
|
||||
}
|
||||
Row(modifier.then(if (onClick != null) Modifier.clickable(onClick = onClick) else Modifier)
|
||||
.height(IntrinsicSize.Min).padding(vertical = 4.dp)) {
|
||||
Box(Modifier.width(3.dp).fillMaxHeight().background(MaterialTheme.colorScheme.primary))
|
||||
Column(Modifier.padding(horizontal = 8.dp).weight(1f)) {
|
||||
Text(original?.let { if (it.isOutgoing) stringResource(R.string.you) else it.author } ?: stringResource(R.string.reply),
|
||||
style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.primary,
|
||||
maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
Text(preview, style = MaterialTheme.typography.bodySmall, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -56,7 +56,7 @@ fun CreatePollDialog(onDismiss: () -> Unit, onCreate: (String, List<String>, Boo
|
||||
|
||||
@Composable
|
||||
fun PollMessageBubble(message: Message, onVote: (Message, List<Int>, String) -> Unit, authorName: (Long) -> String,
|
||||
footer: @Composable () -> Unit = {}) {
|
||||
footer: @Composable () -> Unit = {}, header: @Composable () -> Unit = {}) {
|
||||
var showVote by rememberSaveable(message.messageId) { mutableStateOf(false) }
|
||||
val poll = message.feedback?.poll
|
||||
Row(Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 2.dp),
|
||||
@@ -66,6 +66,7 @@ fun PollMessageBubble(message: Message, onVote: (Message, List<Int>, String) ->
|
||||
else MaterialTheme.colorScheme.surfaceVariant)) {
|
||||
MessageWithFooter(message, footer, Modifier.padding(12.dp)) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
header()
|
||||
if (!message.isOutgoing) Text(message.author, style = MaterialTheme.typography.labelMedium)
|
||||
if (poll == null) Text(stringResource(R.string.poll_pending))
|
||||
else {
|
||||
|
||||
Reference in new issue
Block a user