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() @Composable fun LinkPreview( url: String, modifier: Modifier = Modifier, contentColor: Color = Color.White ) { var metadata by remember(url) { mutableStateOf(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 ) } } }