diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/MessageBubble.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/MessageBubble.kt index 6ed7eead..a7d67fa7 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/MessageBubble.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/MessageBubble.kt @@ -158,62 +158,62 @@ fun MessageBubble(message: Message, playbackState: ChatViewModel.PlaybackState = horizontalArrangement = alignment ) { MessageWithFooter(footer) { - Column( - modifier = Modifier - .widthIn(min = minBubbleWidth, max = if (isVoice) voiceMaxWidth else 300.dp) - // Тень рисуем до clip, чтобы выступающие справа и снизу края оставались видны. - .drawBehind { - val offset = 2.dp.toPx() - val radius = 12.dp.toPx() - drawRoundRect(ChatColors.BubbleShadow, topLeft = Offset(offset, offset), size = size, - cornerRadius = CornerRadius(radius, radius)) - } - .clip(RoundedCornerShape(12.dp)) - .background(bgColor) - .padding(12.dp) - ) { - 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)) + Column( + modifier = Modifier + .widthIn(min = minBubbleWidth, max = if (isVoice) voiceMaxWidth else 300.dp) + // Тень рисуем до clip, чтобы выступающие справа и снизу края оставались видны. + .drawBehind { + val offset = 2.dp.toPx() + val radius = 12.dp.toPx() + drawRoundRect(ChatColors.BubbleShadow, topLeft = Offset(offset, offset), size = size, + cornerRadius = CornerRadius(radius, radius)) + } + .clip(RoundedCornerShape(12.dp)) + .background(bgColor) + .padding(12.dp) + ) { + 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)) - when { - message.inviteGroupName != null -> { - val clipboard = LocalClipboardManager.current - LaunchedEffect(message.id, message.inviteGroupName) { - LogManager.addLog("DEBUG", "PM", "invitation rendered: seq=${message.id} " + - "group_name_bytes=${message.inviteGroupName.toByteArray().size} link_bytes=${message.text.length}") + when { + message.inviteGroupName != null -> { + val clipboard = LocalClipboardManager.current + LaunchedEffect(message.id, message.inviteGroupName) { + LogManager.addLog("DEBUG", "PM", "invitation rendered: seq=${message.id} " + + "group_name_bytes=${message.inviteGroupName.toByteArray().size} link_bytes=${message.text.length}") + } + Text(stringResource(R.string.group_invitation, message.inviteGroupName), fontSize = 17.sp, color = textColor) + Text(message.text, fontSize = 17.sp, color = Color(0xFF3390EC), textDecoration = TextDecoration.Underline, + maxLines = 1, overflow = TextOverflow.Ellipsis, + modifier = Modifier.fillMaxWidth().combinedClickable( + onClick = { onLink(message.text) }, + onLongClickLabel = stringResource(R.string.copy_link), + onLongClick = { + clipboard.setText(AnnotatedString(message.text)) + LogManager.addLog("DEBUG", "PM", "invitation link copied: seq=${message.id} bytes=${message.text.length}") + })) + } + isVoice -> VoiceBubble(message, textColor, isActive, playbackState, onPlayVoice, onSeekVoice, onDownloadFile) + isVideo -> VideoBubble(message, textColor, onDownloadFile, onPlayVideo) + isImage -> ImageBubble(message, textColor, onDownloadFile, onOpenImage) + isMedia -> FileBubble(message, textColor, onDownloadFile, onOpenFile) + else -> { + EmojiText(message.text, fontSize = 17.sp, color = textColor, onLink = onLink) } - Text(stringResource(R.string.group_invitation, message.inviteGroupName), fontSize = 17.sp, color = textColor) - Text(message.text, fontSize = 17.sp, color = Color(0xFF3390EC), textDecoration = TextDecoration.Underline, - maxLines = 1, overflow = TextOverflow.Ellipsis, - modifier = Modifier.fillMaxWidth().combinedClickable( - onClick = { onLink(message.text) }, - onLongClickLabel = stringResource(R.string.copy_link), - onLongClick = { - clipboard.setText(AnnotatedString(message.text)) - LogManager.addLog("DEBUG", "PM", "invitation link copied: seq=${message.id} bytes=${message.text.length}") - })) - } - isVoice -> VoiceBubble(message, textColor, isActive, playbackState, onPlayVoice, onSeekVoice, onDownloadFile) - isVideo -> VideoBubble(message, textColor, onDownloadFile, onPlayVideo) - isImage -> ImageBubble(message, textColor, onDownloadFile, onOpenImage) - isMedia -> FileBubble(message, textColor, onDownloadFile, onOpenFile) - else -> { - EmojiText(message.text, fontSize = 17.sp, color = textColor, onLink = onLink) } - } - if (!isVoice) { - Spacer(Modifier.height(2.dp)) - Text( - listOf(formatTime(message.ts), deliveryLabel(message)).filter { it.isNotEmpty() }.joinToString(" · "), - fontSize = 11.sp, - color = ChatColors.Secondary, - modifier = Modifier.align(Alignment.End) - ) + if (!isVoice) { + Spacer(Modifier.height(2.dp)) + Text( + listOf(formatTime(message.ts), deliveryLabel(message)).filter { it.isNotEmpty() }.joinToString(" · "), + fontSize = 11.sp, + color = ChatColors.Secondary, + modifier = Modifier.align(Alignment.End) + ) + } } } - } } } @Composable diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/PollUi.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/PollUi.kt index 30833dca..dafc9c36 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/PollUi.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/PollUi.kt @@ -60,31 +60,31 @@ fun PollMessageBubble(message: Message, onVote: (Message, List, String) -> val poll = message.feedback?.poll Row(Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 2.dp), horizontalArrangement = if (message.isOutgoing) Arrangement.End else Arrangement.Start) { - MessageWithFooter(footer) { - Card(onClick = { if (poll != null) showVote = true }, modifier = Modifier.widthIn(max = 300.dp), - colors = CardDefaults.cardColors(containerColor = if (message.isOutgoing) MaterialTheme.colorScheme.primaryContainer - else MaterialTheme.colorScheme.surfaceVariant)) { - Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) { - if (!message.isOutgoing) Text(message.author, style = MaterialTheme.typography.labelMedium) - if (poll == null) Text(stringResource(R.string.poll_pending)) - else { - Text(poll.question, style = MaterialTheme.typography.titleMedium) - Text(stringResource(if (poll.multiple) R.string.poll_multiple else R.string.poll_single), - style = MaterialTheme.typography.labelSmall) - for (option in poll.options) { - Text(option.text) - LinearProgressIndicator(progress = { (option.percent / 100).toFloat().coerceIn(0f, 1f) }, - modifier = Modifier.fillMaxWidth()) - Text(stringResource(R.string.poll_option_result, option.count, option.percent), - style = MaterialTheme.typography.labelSmall) + MessageWithFooter(footer) { + Card(onClick = { if (poll != null) showVote = true }, modifier = Modifier.widthIn(max = 300.dp), + colors = CardDefaults.cardColors(containerColor = if (message.isOutgoing) MaterialTheme.colorScheme.primaryContainer + else MaterialTheme.colorScheme.surfaceVariant)) { + Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) { + if (!message.isOutgoing) Text(message.author, style = MaterialTheme.typography.labelMedium) + if (poll == null) Text(stringResource(R.string.poll_pending)) + else { + Text(poll.question, style = MaterialTheme.typography.titleMedium) + Text(stringResource(if (poll.multiple) R.string.poll_multiple else R.string.poll_single), + style = MaterialTheme.typography.labelSmall) + for (option in poll.options) { + Text(option.text) + LinearProgressIndicator(progress = { (option.percent / 100).toFloat().coerceIn(0f, 1f) }, + modifier = Modifier.fillMaxWidth()) + Text(stringResource(R.string.poll_option_result, option.count, option.percent), + style = MaterialTheme.typography.labelSmall) + } + Text(stringResource(R.string.poll_voters, poll.voters)) + TextButton(onClick = { showVote = true }) { Text(stringResource(R.string.poll_vote_results)) } + } } - Text(stringResource(R.string.poll_voters, poll.voters)) - TextButton(onClick = { showVote = true }) { Text(stringResource(R.string.poll_vote_results)) } } } } - } - } if (showVote && poll != null) PollVoteDialog(message, poll, { showVote = false }, onVote, authorName) } diff --git a/tools/chatgui/src/messagedelegate.cpp b/tools/chatgui/src/messagedelegate.cpp index 80f950f5..6ef22693 100644 --- a/tools/chatgui/src/messagedelegate.cpp +++ b/tools/chatgui/src/messagedelegate.cpp @@ -137,6 +137,7 @@ bool MessageDelegate::editorEvent(QEvent *event, QAbstractItemModel *model, Layout L = calcLayout(option, index); const auto* mouse = static_cast(event); + if (mouse->button() != Qt::LeftButton) return false; const QPoint point = mouse->pos() - option.rect.topLeft(); int reaction = L.reactionHeart.contains(point) ? -1 : -2; for (const auto& chip : L.reactionChips) if (chip.rect.contains(point)) { reaction = chip.id; break; } @@ -147,13 +148,13 @@ bool MessageDelegate::editorEvent(QEvent *event, QAbstractItemModel *model, m_avatarPress=onAvatar ? index : QModelIndex(); m_avatarPressPosition=mouse->pos(); return onAvatar || onReaction; } const bool hadReactionPress = m_reactionPress.isValid(); + const bool activateAvatar=onAvatar && m_avatarPress==index && (mouse->pos()-m_avatarPressPosition).manhattanLength()<5; + m_avatarPress=QPersistentModelIndex(); const bool activateReaction = onReaction && m_reactionPress == index && m_pressedReaction == reaction && (mouse->pos() - m_reactionPressPosition).manhattanLength() < 5; m_reactionPress = QPersistentModelIndex(); m_pressedReaction = -2; if (activateReaction) { emit reactionRequested(index, reaction); return true; } if (hadReactionPress || onReaction || (L.reactionHeart.isValid() && point.y() >= L.reactionHeart.top())) return true; - const bool activateAvatar=onAvatar && m_avatarPress==index && (mouse->pos()-m_avatarPressPosition).manhattanLength()<5; - m_avatarPress=QPersistentModelIndex(); if (activateAvatar) { emit avatarRequested(index.data(MsgChannelIdRole).toString(), index.data(MsgAuthorNodeIdRole).toULongLong()); return true;