Browse Source

Keep message footer layout and click gesture states consistent

master
evgeny 5 days ago
parent
commit
fc79fa7345
  1. 100
      tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/MessageBubble.kt
  2. 42
      tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/PollUi.kt
  3. 5
      tools/chatgui/src/messagedelegate.cpp

100
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 horizontalArrangement = alignment
) { ) {
MessageWithFooter(footer) { MessageWithFooter(footer) {
Column( Column(
modifier = Modifier modifier = Modifier
.widthIn(min = minBubbleWidth, max = if (isVoice) voiceMaxWidth else 300.dp) .widthIn(min = minBubbleWidth, max = if (isVoice) voiceMaxWidth else 300.dp)
// Тень рисуем до clip, чтобы выступающие справа и снизу края оставались видны. // Тень рисуем до clip, чтобы выступающие справа и снизу края оставались видны.
.drawBehind { .drawBehind {
val offset = 2.dp.toPx() val offset = 2.dp.toPx()
val radius = 12.dp.toPx() val radius = 12.dp.toPx()
drawRoundRect(ChatColors.BubbleShadow, topLeft = Offset(offset, offset), size = size, drawRoundRect(ChatColors.BubbleShadow, topLeft = Offset(offset, offset), size = size,
cornerRadius = CornerRadius(radius, radius)) cornerRadius = CornerRadius(radius, radius))
} }
.clip(RoundedCornerShape(12.dp)) .clip(RoundedCornerShape(12.dp))
.background(bgColor) .background(bgColor)
.padding(12.dp) .padding(12.dp)
) { ) {
if (!isVoice) Text(if (message.isOutgoing) stringResource(R.string.you) else message.author, if (!isVoice) Text(if (message.isOutgoing) stringResource(R.string.you) else message.author,
fontSize = 13.sp, fontWeight = FontWeight.Bold, color = ChatColors.Secondary) fontSize = 13.sp, fontWeight = FontWeight.Bold, color = ChatColors.Secondary)
Spacer(Modifier.height(2.dp)) Spacer(Modifier.height(2.dp))
when { when {
message.inviteGroupName != null -> { message.inviteGroupName != null -> {
val clipboard = LocalClipboardManager.current val clipboard = LocalClipboardManager.current
LaunchedEffect(message.id, message.inviteGroupName) { LaunchedEffect(message.id, message.inviteGroupName) {
LogManager.addLog("DEBUG", "PM", "invitation rendered: seq=${message.id} " + LogManager.addLog("DEBUG", "PM", "invitation rendered: seq=${message.id} " +
"group_name_bytes=${message.inviteGroupName.toByteArray().size} link_bytes=${message.text.length}") "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) { if (!isVoice) {
Spacer(Modifier.height(2.dp)) Spacer(Modifier.height(2.dp))
Text( Text(
listOf(formatTime(message.ts), deliveryLabel(message)).filter { it.isNotEmpty() }.joinToString(" · "), listOf(formatTime(message.ts), deliveryLabel(message)).filter { it.isNotEmpty() }.joinToString(" · "),
fontSize = 11.sp, fontSize = 11.sp,
color = ChatColors.Secondary, color = ChatColors.Secondary,
modifier = Modifier.align(Alignment.End) modifier = Modifier.align(Alignment.End)
) )
}
} }
} }
}
} }
} }
@Composable @Composable

42
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<Int>, String) ->
val poll = message.feedback?.poll val poll = message.feedback?.poll
Row(Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 2.dp), Row(Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 2.dp),
horizontalArrangement = if (message.isOutgoing) Arrangement.End else Arrangement.Start) { horizontalArrangement = if (message.isOutgoing) Arrangement.End else Arrangement.Start) {
MessageWithFooter(footer) { MessageWithFooter(footer) {
Card(onClick = { if (poll != null) showVote = true }, modifier = Modifier.widthIn(max = 300.dp), Card(onClick = { if (poll != null) showVote = true }, modifier = Modifier.widthIn(max = 300.dp),
colors = CardDefaults.cardColors(containerColor = if (message.isOutgoing) MaterialTheme.colorScheme.primaryContainer colors = CardDefaults.cardColors(containerColor = if (message.isOutgoing) MaterialTheme.colorScheme.primaryContainer
else MaterialTheme.colorScheme.surfaceVariant)) { else MaterialTheme.colorScheme.surfaceVariant)) {
Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) { Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) {
if (!message.isOutgoing) Text(message.author, style = MaterialTheme.typography.labelMedium) if (!message.isOutgoing) Text(message.author, style = MaterialTheme.typography.labelMedium)
if (poll == null) Text(stringResource(R.string.poll_pending)) if (poll == null) Text(stringResource(R.string.poll_pending))
else { else {
Text(poll.question, style = MaterialTheme.typography.titleMedium) Text(poll.question, style = MaterialTheme.typography.titleMedium)
Text(stringResource(if (poll.multiple) R.string.poll_multiple else R.string.poll_single), Text(stringResource(if (poll.multiple) R.string.poll_multiple else R.string.poll_single),
style = MaterialTheme.typography.labelSmall) style = MaterialTheme.typography.labelSmall)
for (option in poll.options) { for (option in poll.options) {
Text(option.text) Text(option.text)
LinearProgressIndicator(progress = { (option.percent / 100).toFloat().coerceIn(0f, 1f) }, LinearProgressIndicator(progress = { (option.percent / 100).toFloat().coerceIn(0f, 1f) },
modifier = Modifier.fillMaxWidth()) modifier = Modifier.fillMaxWidth())
Text(stringResource(R.string.poll_option_result, option.count, option.percent), Text(stringResource(R.string.poll_option_result, option.count, option.percent),
style = MaterialTheme.typography.labelSmall) 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) if (showVote && poll != null) PollVoteDialog(message, poll, { showVote = false }, onVote, authorName)
} }

5
tools/chatgui/src/messagedelegate.cpp

@ -137,6 +137,7 @@ bool MessageDelegate::editorEvent(QEvent *event, QAbstractItemModel *model,
Layout L = calcLayout(option, index); Layout L = calcLayout(option, index);
const auto* mouse = static_cast<QMouseEvent*>(event); const auto* mouse = static_cast<QMouseEvent*>(event);
if (mouse->button() != Qt::LeftButton) return false;
const QPoint point = mouse->pos() - option.rect.topLeft(); const QPoint point = mouse->pos() - option.rect.topLeft();
int reaction = L.reactionHeart.contains(point) ? -1 : -2; int reaction = L.reactionHeart.contains(point) ? -1 : -2;
for (const auto& chip : L.reactionChips) if (chip.rect.contains(point)) { reaction = chip.id; break; } 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; m_avatarPress=onAvatar ? index : QModelIndex(); m_avatarPressPosition=mouse->pos(); return onAvatar || onReaction;
} }
const bool hadReactionPress = m_reactionPress.isValid(); 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 const bool activateReaction = onReaction && m_reactionPress == index && m_pressedReaction == reaction
&& (mouse->pos() - m_reactionPressPosition).manhattanLength() < 5; && (mouse->pos() - m_reactionPressPosition).manhattanLength() < 5;
m_reactionPress = QPersistentModelIndex(); m_pressedReaction = -2; m_reactionPress = QPersistentModelIndex(); m_pressedReaction = -2;
if (activateReaction) { emit reactionRequested(index, reaction); return true; } if (activateReaction) { emit reactionRequested(index, reaction); return true; }
if (hadReactionPress || onReaction || (L.reactionHeart.isValid() && point.y() >= L.reactionHeart.top())) 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) { if (activateAvatar) {
emit avatarRequested(index.data(MsgChannelIdRole).toString(), index.data(MsgAuthorNodeIdRole).toULongLong()); emit avatarRequested(index.data(MsgChannelIdRole).toString(), index.data(MsgAuthorNodeIdRole).toULongLong());
return true; return true;

Loading…
Cancel
Save