Files
forkmessager/client-mobile/chats/presentation/components/LinkPreview.kt
2026-04-16 15:41:22 +03:00

182 lines
7.0 KiB
Kotlin

package chats.presentation.components
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil.compose.AsyncImage
import core.utils.LinkMetadata
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
private val linkMetadataCache = mutableMapOf<String, LinkMetadata>()
@Composable
fun LinkPreview(
url: String,
modifier: Modifier = Modifier,
contentColor: Color = Color.White
) {
var metadata by remember(url) { mutableStateOf<LinkMetadata?>(linkMetadataCache[url]) }
var loading by remember(url) { mutableStateOf(metadata == null) }
val context = LocalContext.current
LaunchedEffect(url) {
if (metadata != null) {
loading = false
return@LaunchedEffect
}
loading = true
try {
val client = okhttp3.OkHttpClient.Builder()
.connectTimeout(10, java.util.concurrent.TimeUnit.SECONDS)
.readTimeout(10, java.util.concurrent.TimeUnit.SECONDS)
.build()
val request = okhttp3.Request.Builder()
.url("https://api.microlink.io?url=${java.net.URLEncoder.encode(url, "UTF-8")}")
.header("User-Agent", "KnotMessenger/1.0 (Android)")
.build()
withContext(Dispatchers.IO) {
client.newCall(request).execute().use { response ->
if (response.isSuccessful) {
val body = response.body?.string()
if (body != null) {
val json = com.google.gson.JsonParser.parseString(body).asJsonObject
if (json.has("status") && json.get("status").asString == "success") {
val data = json.getAsJsonObject("data")
val title = data.get("title")?.takeIf { !it.isJsonNull }?.asString
val description = data.get("description")?.takeIf { !it.isJsonNull }?.asString
val imageUrl = data.getAsJsonObject("image")?.get("url")?.takeIf { !it.isJsonNull }?.asString
if (title != null || description != null || imageUrl != null) {
metadata = LinkMetadata(
url = url,
title = title,
description = description,
imageUrl = imageUrl
)
metadata?.let { linkMetadataCache[url] = it }
}
Unit
}
Unit
}
Unit
} else {
android.util.Log.e("LinkPreview", "API error: ${response.code} ${response.message}")
}
}
Unit
}
} catch (e: Exception) {
android.util.Log.e("LinkPreview", "Failed to fetch metadata for $url", e)
} finally {
loading = false
}
}
if (loading) {
Text(
text = "Загрузка предпросмотра...",
style = MaterialTheme.typography.labelSmall,
color = contentColor.copy(alpha = 0.5f),
modifier = modifier.padding(vertical = 4.dp),
fontStyle = androidx.compose.ui.text.font.FontStyle.Italic
)
return
}
val currentMetadata = metadata ?: LinkMetadata(url = url)
Column(
modifier = modifier
.fillMaxWidth()
.clip(RoundedCornerShape(8.dp))
.background(Color.Black.copy(alpha = 0.2f))
.clickable {
try {
val intent = android.content.Intent(android.content.Intent.ACTION_VIEW, android.net.Uri.parse(url))
context.startActivity(intent)
} catch (e: Exception) {}
}
.border(
width = if (isSystemInDarkTheme()) 0.5.dp else 0.dp,
color = Color.White.copy(alpha = 0.1f),
shape = RoundedCornerShape(8.dp)
)
) {
Row(modifier = Modifier.height(IntrinsicSize.Min)) {
Box(
modifier = Modifier
.fillMaxHeight()
.width(3.dp)
.background(Color(0xFF3096E5))
)
Column(modifier = Modifier.padding(10.dp)) {
val domain = remember(url) {
try { java.net.URL(url).host.replace("www.", "") } catch (e: Exception) { "" }
}
Text(
text = domain,
style = MaterialTheme.typography.labelSmall,
color = Color(0xFF3096E5),
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(bottom = 2.dp)
)
Text(
text = currentMetadata.title ?: url,
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Bold,
color = contentColor,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
currentMetadata.description?.let {
Text(
text = it,
style = MaterialTheme.typography.bodySmall,
color = contentColor.copy(alpha = 0.7f),
maxLines = 3,
overflow = TextOverflow.Ellipsis,
lineHeight = 16.sp
)
}
}
}
currentMetadata.imageUrl?.let {
AsyncImage(
model = it,
contentDescription = null,
modifier = Modifier
.fillMaxWidth()
.heightIn(max = 200.dp)
.clip(RoundedCornerShape(bottomStart = 8.dp, bottomEnd = 8.dp)),
contentScale = ContentScale.Crop
)
}
}
}