feat(android): make chat info entries clickable and open from header
Some checks failed
Android CI / android (push) Has started running
Android Release / release (push) Has been cancelled
CI / test (push) Has been cancelled

This commit is contained in:
Codex
2026-03-10 20:47:14 +03:00
parent e3fdccdeaa
commit 8522e32aea

View File

@@ -2,12 +2,14 @@ package ru.daemonlord.messenger.ui.chat
import android.Manifest import android.Manifest
import android.content.Context import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.media.AudioAttributes import android.media.AudioAttributes
import android.media.MediaMetadataRetriever import android.media.MediaMetadataRetriever
import android.media.MediaPlayer import android.media.MediaPlayer
import android.net.Uri import android.net.Uri
import android.provider.OpenableColumns import android.provider.OpenableColumns
import android.widget.Toast
import android.widget.VideoView import android.widget.VideoView
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
@@ -297,6 +299,7 @@ fun ChatScreen(
onJumpInlineSearch: (Boolean) -> Unit, onJumpInlineSearch: (Boolean) -> Unit,
onVisibleIncomingMessageId: (Long?) -> Unit, onVisibleIncomingMessageId: (Long?) -> Unit,
) { ) {
val context = LocalContext.current
val listState = rememberLazyListState() val listState = rememberLazyListState()
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val allImageUrls = remember(state.messages) { val allImageUrls = remember(state.messages) {
@@ -473,42 +476,52 @@ fun ChatScreen(
contentDescription = "Back", contentDescription = "Back",
) )
} }
if (!state.chatAvatarUrl.isNullOrBlank()) { Row(
AsyncImage( modifier = Modifier
model = state.chatAvatarUrl, .weight(1f)
contentDescription = "Chat avatar for ${state.chatTitle}", .clip(RoundedCornerShape(10.dp))
modifier = Modifier .clickable { showChatInfoSheet = true }
.size(40.dp) .padding(vertical = 2.dp),
.clip(CircleShape), verticalAlignment = Alignment.CenterVertically,
contentScale = ContentScale.Crop, horizontalArrangement = Arrangement.spacedBy(8.dp),
) ) {
} else { if (!state.chatAvatarUrl.isNullOrBlank()) {
Box( AsyncImage(
modifier = Modifier model = state.chatAvatarUrl,
.size(40.dp) contentDescription = "Chat avatar for ${state.chatTitle}",
.clip(CircleShape) modifier = Modifier
.background(MaterialTheme.colorScheme.primaryContainer), .size(40.dp)
contentAlignment = Alignment.Center, .clip(CircleShape),
) { contentScale = ContentScale.Crop,
Text(
text = state.chatTitle.firstOrNull()?.uppercase() ?: "?",
style = MaterialTheme.typography.titleSmall,
) )
} else {
Box(
modifier = Modifier
.size(40.dp)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.primaryContainer),
contentAlignment = Alignment.Center,
) {
Text(
text = state.chatTitle.firstOrNull()?.uppercase() ?: "?",
style = MaterialTheme.typography.titleSmall,
)
}
} }
} Column(modifier = Modifier.weight(1f)) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = state.chatTitle.ifBlank { "Chat #${state.chatId}" },
style = MaterialTheme.typography.titleSmall,
fontWeight = FontWeight.SemiBold,
maxLines = 1,
)
if (state.chatSubtitle.isNotBlank()) {
Text( Text(
text = state.chatSubtitle, text = state.chatTitle.ifBlank { "Chat #${state.chatId}" },
style = MaterialTheme.typography.labelSmall, style = MaterialTheme.typography.titleSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant, fontWeight = FontWeight.SemiBold,
maxLines = 1,
) )
if (state.chatSubtitle.isNotBlank()) {
Text(
text = state.chatSubtitle,
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
} }
} }
IconButton( IconButton(
@@ -1056,6 +1069,32 @@ fun ChatScreen(
ChatInfoTabContent( ChatInfoTabContent(
tab = chatInfoTab, tab = chatInfoTab,
entries = chatInfoEntries, entries = chatInfoEntries,
onEntryClick = { entry ->
when (entry.type) {
ChatInfoEntryType.Media -> {
val url = entry.resourceUrl.orEmpty()
if (url.isBlank()) return@ChatInfoTabContent
if (entry.previewIsVideo) {
viewerVideoUrl = url
} else {
val index = allImageUrls.indexOf(url)
if (index >= 0) {
viewerImageIndex = index
} else {
openUrlExternally(context, url)
}
}
}
ChatInfoEntryType.Link -> {
entry.resourceUrl?.let { openUrlExternally(context, it) }
}
ChatInfoEntryType.File,
ChatInfoEntryType.Voice,
-> {
entry.resourceUrl?.let { openUrlExternally(context, it) }
}
}
},
) )
} }
} }
@@ -2787,6 +2826,18 @@ private fun formatBytes(value: Long): String {
} }
} }
private fun openUrlExternally(context: Context, url: String) {
if (url.isBlank()) return
runCatching {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
context.startActivity(intent)
}.onFailure {
Toast.makeText(context, "Unable to open item", Toast.LENGTH_SHORT).show()
}
}
private enum class ChatInfoTab(val title: String) { private enum class ChatInfoTab(val title: String) {
Media("Media"), Media("Media"),
Files("Files"), Files("Files"),
@@ -2805,6 +2856,8 @@ private data class ChatInfoEntry(
val type: ChatInfoEntryType, val type: ChatInfoEntryType,
val title: String, val title: String,
val subtitle: String, val subtitle: String,
val resourceUrl: String? = null,
val sourceMessageId: Long? = null,
val previewImageUrl: String? = null, val previewImageUrl: String? = null,
val previewIsVideo: Boolean = false, val previewIsVideo: Boolean = false,
) )
@@ -2813,6 +2866,7 @@ private data class ChatInfoEntry(
private fun ChatInfoTabContent( private fun ChatInfoTabContent(
tab: ChatInfoTab, tab: ChatInfoTab,
entries: List<ChatInfoEntry>, entries: List<ChatInfoEntry>,
onEntryClick: (ChatInfoEntry) -> Unit,
) { ) {
val filtered = remember(tab, entries) { val filtered = remember(tab, entries) {
entries.filter { entries.filter {
@@ -2854,7 +2908,8 @@ private fun ChatInfoTabContent(
.fillMaxWidth() .fillMaxWidth()
.aspectRatio(1f) .aspectRatio(1f)
.clip(RoundedCornerShape(4.dp)) .clip(RoundedCornerShape(4.dp))
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.35f)), .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.35f))
.clickable { onEntryClick(entry) },
) { ) {
if (!entry.previewImageUrl.isNullOrBlank()) { if (!entry.previewImageUrl.isNullOrBlank()) {
AsyncImage( AsyncImage(
@@ -2906,6 +2961,7 @@ private fun ChatInfoTabContent(
.fillMaxWidth() .fillMaxWidth()
.clip(RoundedCornerShape(12.dp)) .clip(RoundedCornerShape(12.dp))
.background(MaterialTheme.colorScheme.surface.copy(alpha = 0.55f)) .background(MaterialTheme.colorScheme.surface.copy(alpha = 0.55f))
.clickable { onEntryClick(entry) }
.padding(horizontal = 10.dp, vertical = 9.dp), .padding(horizontal = 10.dp, vertical = 9.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(10.dp), horizontalArrangement = Arrangement.spacedBy(10.dp),
@@ -2982,6 +3038,8 @@ private fun buildChatInfoEntries(messages: List<MessageItem>): List<ChatInfoEntr
type = ChatInfoEntryType.Media, type = ChatInfoEntryType.Media,
title = extractFileName(attachment.fileUrl), title = extractFileName(attachment.fileUrl),
subtitle = "$time${attachment.fileType}", subtitle = "$time${attachment.fileType}",
resourceUrl = attachment.fileUrl,
sourceMessageId = message.id,
previewImageUrl = if (normalized.startsWith("image/")) attachment.fileUrl else null, previewImageUrl = if (normalized.startsWith("image/")) attachment.fileUrl else null,
previewIsVideo = normalized.startsWith("video/"), previewIsVideo = normalized.startsWith("video/"),
) )
@@ -2992,6 +3050,8 @@ private fun buildChatInfoEntries(messages: List<MessageItem>): List<ChatInfoEntr
type = ChatInfoEntryType.Voice, type = ChatInfoEntryType.Voice,
title = extractFileName(attachment.fileUrl), title = extractFileName(attachment.fileUrl),
subtitle = "$time${formatBytes(attachment.fileSize)}", subtitle = "$time${formatBytes(attachment.fileSize)}",
resourceUrl = attachment.fileUrl,
sourceMessageId = message.id,
) )
} }
@@ -3000,6 +3060,8 @@ private fun buildChatInfoEntries(messages: List<MessageItem>): List<ChatInfoEntr
type = ChatInfoEntryType.File, type = ChatInfoEntryType.File,
title = extractFileName(attachment.fileUrl), title = extractFileName(attachment.fileUrl),
subtitle = "$time${formatBytes(attachment.fileSize)}", subtitle = "$time${formatBytes(attachment.fileSize)}",
resourceUrl = attachment.fileUrl,
sourceMessageId = message.id,
) )
} }
} }
@@ -3009,6 +3071,8 @@ private fun buildChatInfoEntries(messages: List<MessageItem>): List<ChatInfoEntr
type = ChatInfoEntryType.Link, type = ChatInfoEntryType.Link,
title = match.value, title = match.value,
subtitle = "${message.senderDisplayName ?: "Unknown"}$time", subtitle = "${message.senderDisplayName ?: "Unknown"}$time",
resourceUrl = match.value,
sourceMessageId = message.id,
) )
} }
} }