diff --git a/android/app/src/main/java/ru/daemonlord/messenger/ui/chat/ChatScreen.kt b/android/app/src/main/java/ru/daemonlord/messenger/ui/chat/ChatScreen.kt index 7a53c7c..440e7c1 100644 --- a/android/app/src/main/java/ru/daemonlord/messenger/ui/chat/ChatScreen.kt +++ b/android/app/src/main/java/ru/daemonlord/messenger/ui/chat/ChatScreen.kt @@ -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, + 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): List): List): List): List