diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/ConversationMessages.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/ConversationMessages.kt index c35a9c42..676adb3f 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/ConversationMessages.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/ConversationMessages.kt @@ -2,8 +2,14 @@ package com.utun.chat.ui.components import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.* -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.* +import android.content.Intent +import android.content.ActivityNotFoundException +import android.net.Uri +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawWithCache import androidx.compose.ui.geometry.Offset @@ -14,11 +20,13 @@ import androidx.compose.ui.graphics.ShaderBrush import androidx.compose.ui.graphics.TileMode import androidx.compose.ui.graphics.drawscope.scale import androidx.compose.ui.res.imageResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.utun.chat.R import com.utun.chat.data.ConversationTarget import com.utun.chat.data.LogManager import com.utun.chat.data.Message +import com.utun.chat.data.MessageLinks import com.utun.chat.viewmodel.ChatViewModel /** Общий список; направление включено в ключ, поскольку PM имеет два потока seq. */ @@ -29,6 +37,30 @@ fun ConversationMessages(target: ConversationTarget, messages: List, st onOpenFile: (Message) -> Unit, onPlayVideo: (Message) -> Unit, onDownloadFile: (Message) -> Unit = {}, onOpenImage: (Message) -> Unit = {}, onInviteLink: (String) -> Unit = {}) { + val context = LocalContext.current + var openFailed by remember { mutableStateOf(false) } + val onLink: (String) -> Unit = { link -> + if (link.startsWith("utun://")) onInviteLink(link) + else if (!MessageLinks.isWebUrl(link)) { + LogManager.addLog("ERROR", "LINK", "invalid external message link: bytes=${link.length}") + openFailed = true + } else { + val uri = Uri.parse(link) + try { + context.startActivity(Intent(Intent.ACTION_VIEW, uri)) + LogManager.addLog("INFO", "LINK", "external open requested: scheme=${uri.scheme} host=${uri.host}") + } catch (e: ActivityNotFoundException) { + LogManager.addLog("ERROR", "LINK", "no URL handler: scheme=${uri.scheme} host=${uri.host} error=${e.message}") + openFailed = true + } catch (e: SecurityException) { + LogManager.addLog("ERROR", "LINK", "URL handler denied: scheme=${uri.scheme} host=${uri.host} error=${e.message}") + openFailed = true + } + } + } + if (openFailed) AlertDialog(onDismissRequest = { openFailed = false }, + text = { Text(stringResource(R.string.link_open_failed)) }, + confirmButton = { TextButton(onClick = { openFailed = false }) { Text(stringResource(R.string.close)) } }) val background = ImageBitmap.imageResource(R.drawable.chat_background) LaunchedEffect(background) { LogManager.addLog("DEBUG", "UI", "Chat background ready: desktop bg.png tile=${background.width}x${background.height}") @@ -45,7 +77,7 @@ fun ConversationMessages(target: ConversationTarget, messages: List, st LazyColumn(modifier = modifier.then(backgroundModifier), state = state, reverseLayout = true) { item { Spacer(Modifier.height(8.dp)) } items(messages.reversed(), key = { "${target.nativeId}:${it.isOutgoing}:${it.id}" }) { message -> - MessageBubble(message, playbackState, onPlayVoice, onSeekVoice, onDownloadFile, onOpenFile, onOpenImage, onPlayVideo, onInviteLink) + MessageBubble(message, playbackState, onPlayVoice, onSeekVoice, onDownloadFile, onOpenFile, onOpenImage, onPlayVideo, onLink) } } }