|
|
|
@ -1,6 +1,8 @@ |
|
|
|
package com.utun.chat.ui.components |
|
|
|
package com.utun.chat.ui.components |
|
|
|
|
|
|
|
|
|
|
|
import androidx.compose.foundation.layout.* |
|
|
|
import androidx.compose.foundation.layout.* |
|
|
|
|
|
|
|
import androidx.compose.foundation.background |
|
|
|
|
|
|
|
import androidx.compose.foundation.clickable |
|
|
|
import androidx.compose.foundation.lazy.* |
|
|
|
import androidx.compose.foundation.lazy.* |
|
|
|
import androidx.compose.runtime.* |
|
|
|
import androidx.compose.runtime.* |
|
|
|
import android.content.Intent |
|
|
|
import android.content.Intent |
|
|
|
@ -9,6 +11,14 @@ import android.net.Uri |
|
|
|
import androidx.compose.material3.AlertDialog |
|
|
|
import androidx.compose.material3.AlertDialog |
|
|
|
import androidx.compose.material3.Text |
|
|
|
import androidx.compose.material3.Text |
|
|
|
import androidx.compose.material3.TextButton |
|
|
|
import androidx.compose.material3.TextButton |
|
|
|
|
|
|
|
import androidx.compose.material3.ExperimentalMaterial3Api |
|
|
|
|
|
|
|
import androidx.compose.material3.ModalBottomSheet |
|
|
|
|
|
|
|
import androidx.compose.material3.ListItem |
|
|
|
|
|
|
|
import androidx.compose.material3.MaterialTheme |
|
|
|
|
|
|
|
import androidx.compose.ui.platform.LocalClipboardManager |
|
|
|
|
|
|
|
import androidx.compose.ui.platform.LocalHapticFeedback |
|
|
|
|
|
|
|
import androidx.compose.ui.hapticfeedback.HapticFeedbackType |
|
|
|
|
|
|
|
import androidx.compose.ui.text.AnnotatedString |
|
|
|
import androidx.compose.ui.platform.LocalContext |
|
|
|
import androidx.compose.ui.platform.LocalContext |
|
|
|
import androidx.compose.ui.Modifier |
|
|
|
import androidx.compose.ui.Modifier |
|
|
|
import androidx.compose.ui.Alignment |
|
|
|
import androidx.compose.ui.Alignment |
|
|
|
@ -32,6 +42,7 @@ import com.utun.chat.viewmodel.ChatViewModel |
|
|
|
|
|
|
|
|
|
|
|
/** Общий список; направление включено в ключ, поскольку PM имеет два потока seq. */ |
|
|
|
/** Общий список; направление включено в ключ, поскольку PM имеет два потока seq. */ |
|
|
|
@Composable |
|
|
|
@Composable |
|
|
|
|
|
|
|
@OptIn(ExperimentalMaterial3Api::class) |
|
|
|
fun ConversationMessages(target: ConversationTarget, messages: List<Message>, state: LazyListState, |
|
|
|
fun ConversationMessages(target: ConversationTarget, messages: List<Message>, state: LazyListState, |
|
|
|
playbackState: ChatViewModel.PlaybackState, modifier: Modifier = Modifier, |
|
|
|
playbackState: ChatViewModel.PlaybackState, modifier: Modifier = Modifier, |
|
|
|
onPlayVoice: (String, Int) -> Unit, onSeekVoice: (String, Int, Int) -> Unit, |
|
|
|
onPlayVoice: (String, Int) -> Unit, onSeekVoice: (String, Int, Int) -> Unit, |
|
|
|
@ -40,8 +51,27 @@ fun ConversationMessages(target: ConversationTarget, messages: List<Message>, st |
|
|
|
onInviteLink: (String) -> Unit = {}, reactionCatalog: List<com.utun.chat.data.ReactionState> = emptyList(), |
|
|
|
onInviteLink: (String) -> Unit = {}, reactionCatalog: List<com.utun.chat.data.ReactionState> = emptyList(), |
|
|
|
onVote: (Message, List<Int>, String) -> Unit = { _, _, _ -> }, |
|
|
|
onVote: (Message, List<Int>, String) -> Unit = { _, _, _ -> }, |
|
|
|
onReact: (Message, Int?) -> Unit = { _, _ -> }, |
|
|
|
onReact: (Message, Int?) -> Unit = { _, _ -> }, |
|
|
|
authorName: (Long) -> String = { "0x%016x".format(it) }) { |
|
|
|
authorName: (Long) -> String = { "0x%016x".format(it) }, |
|
|
|
|
|
|
|
onReply: (Message) -> Unit = {}, onOriginal: (String) -> Unit = {}, |
|
|
|
|
|
|
|
originals: Map<String, Message> = emptyMap(), highlightedId: String = "") { |
|
|
|
val context = LocalContext.current |
|
|
|
val context = LocalContext.current |
|
|
|
|
|
|
|
val clipboard = LocalClipboardManager.current |
|
|
|
|
|
|
|
val haptic = LocalHapticFeedback.current |
|
|
|
|
|
|
|
var selectedMessage by remember(target) { mutableStateOf<Message?>(null) } |
|
|
|
|
|
|
|
selectedMessage?.let { selected -> |
|
|
|
|
|
|
|
ModalBottomSheet(onDismissRequest = { selectedMessage = null }) { |
|
|
|
|
|
|
|
ListItem(headlineContent = { Text(stringResource(R.string.reply)) }, modifier = Modifier.clickable { |
|
|
|
|
|
|
|
selectedMessage = null |
|
|
|
|
|
|
|
onReply(selected) |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
if (selected.contentType == "text/plain") ListItem(headlineContent = { Text(stringResource(R.string.message_copy)) }, |
|
|
|
|
|
|
|
modifier = Modifier.clickable { |
|
|
|
|
|
|
|
clipboard.setText(AnnotatedString(selected.text)) |
|
|
|
|
|
|
|
selectedMessage = null |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
Spacer(Modifier.height(16.dp)) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
var openFailed by remember { mutableStateOf(false) } |
|
|
|
var openFailed by remember { mutableStateOf(false) } |
|
|
|
val onLink: (String) -> Unit = { link -> |
|
|
|
val onLink: (String) -> Unit = { link -> |
|
|
|
if (link.startsWith("utun://")) onInviteLink(link) |
|
|
|
if (link.startsWith("utun://")) onInviteLink(link) |
|
|
|
@ -85,16 +115,26 @@ fun ConversationMessages(target: ConversationTarget, messages: List<Message>, st |
|
|
|
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.Bottom) { |
|
|
|
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.Bottom) { |
|
|
|
if (!message.isOutgoing) UserAvatar(group, message.authorNodeId, message.author, |
|
|
|
if (!message.isOutgoing) UserAvatar(group, message.authorNodeId, message.author, |
|
|
|
modifier = Modifier.padding(start = 8.dp, bottom = 4.dp), size = 54.4.dp) |
|
|
|
modifier = Modifier.padding(start = 8.dp, bottom = 4.dp), size = 54.4.dp) |
|
|
|
Box(Modifier.weight(1f)) { |
|
|
|
val actions = if (target is ConversationTarget.Channel && message.messageId.isNotEmpty()) |
|
|
|
|
|
|
|
Modifier.messageLongPress(stringResource(R.string.message_actions)) { |
|
|
|
|
|
|
|
haptic.performHapticFeedback(HapticFeedbackType.LongPress) |
|
|
|
|
|
|
|
selectedMessage = message |
|
|
|
|
|
|
|
} else Modifier |
|
|
|
|
|
|
|
Box(Modifier.weight(1f).then(actions).then(if (message.messageId == highlightedId && highlightedId.isNotEmpty()) |
|
|
|
|
|
|
|
Modifier.background(MaterialTheme.colorScheme.primary.copy(alpha = 0.18f)) else Modifier)) { |
|
|
|
Column { |
|
|
|
Column { |
|
|
|
val footer: @Composable () -> Unit = { |
|
|
|
val footer: @Composable () -> Unit = { |
|
|
|
if (target is ConversationTarget.Channel && message.messageId.isNotEmpty()) |
|
|
|
if (target is ConversationTarget.Channel && message.messageId.isNotEmpty()) |
|
|
|
MessageReactions(message, reactionCatalog, onReact) |
|
|
|
MessageReactions(message, reactionCatalog, onReact) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
val header: @Composable () -> Unit = { |
|
|
|
|
|
|
|
if (target is ConversationTarget.Channel && message.replyTo.isNotEmpty()) |
|
|
|
|
|
|
|
ReplyQuote(originals[message.replyTo], Modifier.fillMaxWidth()) { onOriginal(message.replyTo) } |
|
|
|
|
|
|
|
} |
|
|
|
if (target is ConversationTarget.Channel && message.contentType == "poll") |
|
|
|
if (target is ConversationTarget.Channel && message.contentType == "poll") |
|
|
|
PollMessageBubble(message, onVote, authorName, footer) |
|
|
|
PollMessageBubble(message, onVote, authorName, footer, header) |
|
|
|
else MessageBubble(message, playbackState, onPlayVoice, onSeekVoice, onDownloadFile, |
|
|
|
else MessageBubble(message, playbackState, onPlayVoice, onSeekVoice, onDownloadFile, |
|
|
|
onOpenFile, onOpenImage, onPlayVideo, onLink, footer) |
|
|
|
onOpenFile, onOpenImage, onPlayVideo, onLink, footer, header) |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
if (message.isOutgoing) UserAvatar(group, message.authorNodeId, message.author, |
|
|
|
if (message.isOutgoing) UserAvatar(group, message.authorNodeId, message.author, |
|
|
|
|