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.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.media.AudioAttributes
import android.media.MediaMetadataRetriever
import android.media.MediaPlayer
import android.net.Uri
import android.provider.OpenableColumns
import android.widget.Toast
import android.widget.VideoView
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
@@ -297,6 +299,7 @@ fun ChatScreen(
onJumpInlineSearch: (Boolean) -> Unit,
onVisibleIncomingMessageId: (Long?) -> Unit,
) {
val context = LocalContext.current
val listState = rememberLazyListState()
val scope = rememberCoroutineScope()
val allImageUrls = remember(state.messages) {
@@ -473,42 +476,52 @@ fun ChatScreen(
contentDescription = "Back",
)
}
if (!state.chatAvatarUrl.isNullOrBlank()) {
AsyncImage(
model = state.chatAvatarUrl,
contentDescription = "Chat avatar for ${state.chatTitle}",
modifier = Modifier
.size(40.dp)
.clip(CircleShape),
contentScale = ContentScale.Crop,
)
} 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,
Row(
modifier = Modifier
.weight(1f)
.clip(RoundedCornerShape(10.dp))
.clickable { showChatInfoSheet = true }
.padding(vertical = 2.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
if (!state.chatAvatarUrl.isNullOrBlank()) {
AsyncImage(
model = state.chatAvatarUrl,
contentDescription = "Chat avatar for ${state.chatTitle}",
modifier = Modifier
.size(40.dp)
.clip(CircleShape),
contentScale = ContentScale.Crop,
)
} 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)) {
Text(
text = state.chatTitle.ifBlank { "Chat #${state.chatId}" },
style = MaterialTheme.typography.titleSmall,
fontWeight = FontWeight.SemiBold,
maxLines = 1,
)
if (state.chatSubtitle.isNotBlank()) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = state.chatSubtitle,
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
text = state.chatTitle.ifBlank { "Chat #${state.chatId}" },
style = MaterialTheme.typography.titleSmall,
fontWeight = FontWeight.SemiBold,
maxLines = 1,
)
if (state.chatSubtitle.isNotBlank()) {
Text(
text = state.chatSubtitle,
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
}
IconButton(
@@ -1056,6 +1069,32 @@ fun ChatScreen(
ChatInfoTabContent(
tab = chatInfoTab,
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) {
Media("Media"),
Files("Files"),
@@ -2805,6 +2856,8 @@ private data class ChatInfoEntry(
val type: ChatInfoEntryType,
val title: String,
val subtitle: String,
val resourceUrl: String? = null,
val sourceMessageId: Long? = null,
val previewImageUrl: String? = null,
val previewIsVideo: Boolean = false,
)
@@ -2813,6 +2866,7 @@ private data class ChatInfoEntry(
private fun ChatInfoTabContent(
tab: ChatInfoTab,
entries: List<ChatInfoEntry>,
onEntryClick: (ChatInfoEntry) -> Unit,
) {
val filtered = remember(tab, entries) {
entries.filter {
@@ -2854,7 +2908,8 @@ private fun ChatInfoTabContent(
.fillMaxWidth()
.aspectRatio(1f)
.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()) {
AsyncImage(
@@ -2906,6 +2961,7 @@ private fun ChatInfoTabContent(
.fillMaxWidth()
.clip(RoundedCornerShape(12.dp))
.background(MaterialTheme.colorScheme.surface.copy(alpha = 0.55f))
.clickable { onEntryClick(entry) }
.padding(horizontal = 10.dp, vertical = 9.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(10.dp),
@@ -2982,6 +3038,8 @@ private fun buildChatInfoEntries(messages: List<MessageItem>): List<ChatInfoEntr
type = ChatInfoEntryType.Media,
title = extractFileName(attachment.fileUrl),
subtitle = "$time${attachment.fileType}",
resourceUrl = attachment.fileUrl,
sourceMessageId = message.id,
previewImageUrl = if (normalized.startsWith("image/")) attachment.fileUrl else null,
previewIsVideo = normalized.startsWith("video/"),
)
@@ -2992,6 +3050,8 @@ private fun buildChatInfoEntries(messages: List<MessageItem>): List<ChatInfoEntr
type = ChatInfoEntryType.Voice,
title = extractFileName(attachment.fileUrl),
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,
title = extractFileName(attachment.fileUrl),
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,
title = match.value,
subtitle = "${message.senderDisplayName ?: "Unknown"}$time",
resourceUrl = match.value,
sourceMessageId = message.id,
)
}
}