Browse Source

android: render reply quotes and support message long press

master
evgeny 4 days ago
parent
commit
30102f5f42
  1. 14
      tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/MessageBubble.kt
  2. 70
      tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/MessageReply.kt
  3. 3
      tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/PollUi.kt

14
tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/MessageBubble.kt

@ -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)

70
tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/MessageReply.kt

@ -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)
}
}
}

3
tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/PollUi.kt

@ -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 {

Loading…
Cancel
Save