diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ComposeView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ComposeView.kt index f2a3781776..9106b249fd 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ComposeView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ComposeView.kt @@ -294,6 +294,9 @@ fun ComposeView( if (lp != null && pendingLinkUrl.value == url) { composeState.value = composeState.value.copy(preview = ComposePreview.CLinkPreview(lp)) pendingLinkUrl.value = null + } else if (pendingLinkUrl.value == url) { + composeState.value = composeState.value.copy(preview = ComposePreview.NoPreview) + pendingLinkUrl.value = null } } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/LinkPreviews.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/LinkPreviews.kt index f101b7c916..47b06b42d1 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/LinkPreviews.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/LinkPreviews.kt @@ -26,29 +26,44 @@ import chat.simplex.app.views.chat.item.SentColorLight import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.jsoup.Jsoup +import java.net.URL private const val OG_SELECT_QUERY = "meta[property^=og:]" +private const val ICON_SELECT_QUERY = "link[rel^=icon],link[rel^=apple-touch-icon],link[rel^=shortcut icon]" +private val IMAGE_SUFFIXES = listOf(".jpg", ".png", ".ico", ".webp", ".gif") suspend fun getLinkPreview(url: String): LinkPreview? { return withContext(Dispatchers.IO) { try { - val response = Jsoup.connect(url) - .ignoreContentType(true) - .timeout(10000) - .followRedirects(true) - .execute() - val doc = response.parse() - val ogTags = doc.select(OG_SELECT_QUERY) - val imageUri = ogTags.firstOrNull { it.attr("property") == "og:image" }?.attr("content") + val title: String? + val u = kotlin.runCatching { URL(url) }.getOrNull() ?: return@withContext null + var imageUri = when { + IMAGE_SUFFIXES.any { u.path.lowercase().endsWith(it) } -> { + title = u.path.substringAfterLast("/") + url + } + else -> { + val response = Jsoup.connect(url) + .ignoreContentType(true) + .timeout(10000) + .followRedirects(true) + .execute() + val doc = response.parse() + val ogTags = doc.select(OG_SELECT_QUERY) + title = ogTags.firstOrNull { it.attr("property") == "og:title" }?.attr("content") ?: doc.title() + ogTags.firstOrNull { it.attr("property") == "og:image" }?.attr("content") + ?: doc.select(ICON_SELECT_QUERY).firstOrNull { it.attr("rel").contains("icon") }?.attr("href") + } + } if (imageUri != null) { + imageUri = normalizeImageUri(u, imageUri) try { - val stream = java.net.URL(imageUri).openStream() + val stream = URL(imageUri).openStream() val image = resizeImageToStrSize(BitmapFactory.decodeStream(stream), maxDataSize = 14000) // TODO add once supported in iOS // val description = ogTags.firstOrNull { // it.attr("property") == "og:description" // }?.attr("content") ?: "" - val title = ogTags.firstOrNull { it.attr("property") == "og:title" }?.attr("content") if (title != null) { return@withContext LinkPreview(url, title, description = "", image) } @@ -63,8 +78,6 @@ suspend fun getLinkPreview(url: String): LinkPreview? { } } - - @Composable fun ComposeLinkView(linkPreview: LinkPreview?, cancelPreview: () -> Unit) { Row( @@ -127,6 +140,37 @@ fun ChatItemLinkView(linkPreview: LinkPreview) { } } +private fun normalizeImageUri(u: URL, imageUri: String) = when { + !imageUri.lowercase().startsWith("http") -> { + "${u.protocol}://${u.host}" + + if (imageUri.startsWith("/")) + imageUri + else + // When an icon is used as an image with relative href path like: site=site.com, + if (u.path.endsWith("/")) u.path + imageUri + else u.path.substringBeforeLast("/") + "/$imageUri" + } + else -> imageUri +} + +/*fun normalizeImageUriTest() { + val expect = mapOf, String>( + URL("https://example.com") to "icon.png" to "https://example.com/icon.png", + URL("https://example.com/") to "icon.png" to "https://example.com/icon.png", + URL("https://example.com/") to "/icon.png" to "https://example.com/icon.png", + URL("https://example.com") to "assets/images/favicon.png" to "https://example.com/assets/images/favicon.png", + URL("https://example.com/") to "assets/images/favicon.png" to "https://example.com/assets/images/favicon.png", + URL("https://example.com/dir") to "/favicon.png" to "https://example.com/favicon.png", + URL("https://example.com/dir/") to "favicon.png" to "https://example.com/dir/favicon.png", + URL("https://example.com/dir/") to "/favicon.png" to "https://example.com/favicon.png", + URL("https://example.com/dir/page") to "favicon.png" to "https://example.com/dir/favicon.png", + URL("https://example.com/abcde.gif") to "https://example.com/abcde.gif" to "https://example.com/abcde.gif", + URL("https://example.com/abcde.gif?a=b") to "https://example.com/abcde.gif?a=b" to "https://example.com/abcde.gif?a=b", + ) + expect.forEach { + Log.d(TAG, "Image URI ${normalizeImageUri(it.key.first, it.key.second)} == ${normalizeImageUri(it.key.first, it.key.second) == it.value}") + } +}*/ @Preview(showBackground = true) @Preview(