From 3a7b503890fe87e87098d7b956585174f4fd40b4 Mon Sep 17 00:00:00 2001 From: greenart7c3 Date: Mon, 1 Jul 2024 14:25:29 -0300 Subject: [PATCH] Show base64 images --- .../amethyst/ui/components/RichTextViewer.kt | 51 +++++++++++++++++++ .../ExpandableTextCutOffCalculator.kt | 3 -- .../commons/richtext/RichTextParser.kt | 26 +++++++++- .../richtext/RichTextParserSegments.kt | 4 ++ 4 files changed, 79 insertions(+), 5 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt index 66daf83e2..b86990d5c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt @@ -20,6 +20,8 @@ */ package com.vitorpamplona.amethyst.ui.components +import android.util.Base64 +import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -28,7 +30,10 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.text.InlineTextContent import androidx.compose.foundation.text.appendInlineContent import androidx.compose.material3.Icon @@ -47,6 +52,8 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalFontFamilyResolver import androidx.compose.ui.platform.LocalLayoutDirection @@ -61,7 +68,12 @@ import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.em import androidx.lifecycle.viewmodel.compose.viewModel +import coil.compose.AsyncImagePainter +import coil.compose.SubcomposeAsyncImage +import coil.compose.SubcomposeAsyncImageContent +import coil.request.ImageRequest import com.vitorpamplona.amethyst.commons.compose.produceCachedState +import com.vitorpamplona.amethyst.commons.richtext.Base64Segment import com.vitorpamplona.amethyst.commons.richtext.BechSegment import com.vitorpamplona.amethyst.commons.richtext.CashuSegment import com.vitorpamplona.amethyst.commons.richtext.EmailSegment @@ -74,6 +86,7 @@ import com.vitorpamplona.amethyst.commons.richtext.InvoiceSegment import com.vitorpamplona.amethyst.commons.richtext.LinkSegment import com.vitorpamplona.amethyst.commons.richtext.PhoneSegment import com.vitorpamplona.amethyst.commons.richtext.RegularTextSegment +import com.vitorpamplona.amethyst.commons.richtext.RichTextParser import com.vitorpamplona.amethyst.commons.richtext.RichTextViewerState import com.vitorpamplona.amethyst.commons.richtext.SchemelessUrlSegment import com.vitorpamplona.amethyst.commons.richtext.Segment @@ -86,6 +99,7 @@ import com.vitorpamplona.amethyst.model.checkForHashtagWithIcon import com.vitorpamplona.amethyst.service.CachedRichTextParser import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled import com.vitorpamplona.amethyst.ui.components.markdown.RenderContentAsMarkdown +import com.vitorpamplona.amethyst.ui.note.BlankNote import com.vitorpamplona.amethyst.ui.note.LoadUser import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.note.toShortenHex @@ -433,6 +447,43 @@ private fun RenderWordWithPreview( is HashIndexEventSegment -> TagLink(word, true, quotesLeft, backgroundColor, accountViewModel, nav) is SchemelessUrlSegment -> NoProtocolUrlRenderer(word) is RegularTextSegment -> Text(word.segmentText) + is Base64Segment -> ImageFromBase64(word.segmentText) + } +} + +@Composable +fun ImageFromBase64(base64String: String) { + val context = LocalContext.current + + var base64String2 = base64String.removePrefix("data:image/jpeg;base64,") + RichTextParser.imageExtensions.forEach { + base64String2 = base64String2.removePrefix("data:image/$it;base64,") + } + + val imageBytes = runCatching { Base64.decode(base64String2, Base64.DEFAULT) }.getOrNull() + + if (imageBytes == null) { + BlankNote() + } else { + val request = + ImageRequest.Builder(context) + .data(imageBytes) + .build() + + SubcomposeAsyncImage( + model = request, + contentDescription = null, + contentScale = ContentScale.FillWidth, + modifier = Modifier.fillMaxWidth(), + ) { + when (painter.state) { + is AsyncImagePainter.State.Success -> { + SubcomposeAsyncImageContent() + } + + else -> BlankNote() + } + } } } diff --git a/commons/src/main/java/com/vitorpamplona/amethyst/commons/richtext/ExpandableTextCutOffCalculator.kt b/commons/src/main/java/com/vitorpamplona/amethyst/commons/richtext/ExpandableTextCutOffCalculator.kt index 9a91c8ae5..e537ce840 100644 --- a/commons/src/main/java/com/vitorpamplona/amethyst/commons/richtext/ExpandableTextCutOffCalculator.kt +++ b/commons/src/main/java/com/vitorpamplona/amethyst/commons/richtext/ExpandableTextCutOffCalculator.kt @@ -43,9 +43,6 @@ class ExpandableTextCutOffCalculator { newString.lastIndexOf(' ').let { if (it < 0) content.length else it } val firstNewLineBeforeCut = newString.lastIndexOf('\n').let { if (it < 0) content.length else it } - if (maxOf(firstSpaceBeforeCut, firstNewLineBeforeCut) == content.length && content.length > SHORT_TEXT_LENGTH) { - return SHORT_TEXT_LENGTH - } return maxOf(firstSpaceBeforeCut, firstNewLineBeforeCut) } else { diff --git a/commons/src/main/java/com/vitorpamplona/amethyst/commons/richtext/RichTextParser.kt b/commons/src/main/java/com/vitorpamplona/amethyst/commons/richtext/RichTextParser.kt index 226263367..44e51df8f 100644 --- a/commons/src/main/java/com/vitorpamplona/amethyst/commons/richtext/RichTextParser.kt +++ b/commons/src/main/java/com/vitorpamplona/amethyst/commons/richtext/RichTextParser.kt @@ -81,6 +81,21 @@ class RichTextParser { } } + private fun parseBase64Images(content: String): LinkedHashSet { + val regex = "data:image/(${imageExtensions.joinToString(separator = "|") { it } });base64,[a-zA-Z0-9+/]+={0,2}" + val pattern = Pattern.compile(regex) + val matcher = pattern.matcher(content) + + val base64Images = mutableListOf() + + // Find all matches and add them to the list + while (matcher.find()) { + base64Images.add(matcher.group()) + } + + return base64Images.mapTo(LinkedHashSet(base64Images.size)) { it } + } + fun parseValidUrls(content: String): LinkedHashSet { val urls = UrlDetector(content, UrlDetectorOptions.Default).detect() @@ -112,13 +127,15 @@ class RichTextParser { ): RichTextViewerState { val urlSet = parseValidUrls(content) + val base64Images = parseBase64Images(content) + val imagesForPager = urlSet.mapNotNull { fullUrl -> parseMediaUrl(fullUrl, tags, content, callbackUri) }.associateBy { it.url } val imageList = imagesForPager.values.toList() val emojiMap = Nip30CustomEmoji.createEmojiMap(tags) - val segments = findTextSegments(content, imagesForPager.keys, urlSet, emojiMap, tags) + val segments = findTextSegments(content, imagesForPager.keys, urlSet, emojiMap, tags, base64Images) return RichTextViewerState( urlSet.toImmutableSet(), @@ -126,6 +143,7 @@ class RichTextParser { imageList.toImmutableList(), emojiMap.toImmutableMap(), segments, + base64Images.toImmutableSet(), ) } @@ -135,6 +153,7 @@ class RichTextParser { urls: Set, emojis: Map, tags: ImmutableListOfLists, + base64Images: Set, ): ImmutableList { val lines = content.split('\n') val paragraphSegments = ArrayList(lines.size) @@ -146,7 +165,7 @@ class RichTextParser { val wordList = paragraph.trimEnd().split(' ') val segments = ArrayList(wordList.size) wordList.forEach { word -> - val wordSegment = wordIdentifier(word, images, urls, emojis, tags) + val wordSegment = wordIdentifier(word, images, urls, emojis, tags, base64Images) if (wordSegment !is RegularTextSegment) { isDirty = true } @@ -200,9 +219,12 @@ class RichTextParser { urls: Set, emojis: Map, tags: ImmutableListOfLists, + base64Images: Set, ): Segment { if (word.isEmpty()) return RegularTextSegment(word) + if (base64Images.contains(word)) return Base64Segment(word) + if (images.contains(word)) return ImageSegment(word) if (urls.contains(word)) return LinkSegment(word) diff --git a/commons/src/main/java/com/vitorpamplona/amethyst/commons/richtext/RichTextParserSegments.kt b/commons/src/main/java/com/vitorpamplona/amethyst/commons/richtext/RichTextParserSegments.kt index 4937c1f40..e4a38683f 100644 --- a/commons/src/main/java/com/vitorpamplona/amethyst/commons/richtext/RichTextParserSegments.kt +++ b/commons/src/main/java/com/vitorpamplona/amethyst/commons/richtext/RichTextParserSegments.kt @@ -32,6 +32,7 @@ data class RichTextViewerState( val imageList: ImmutableList, val customEmoji: ImmutableMap, val paragraphs: ImmutableList, + val base64Images: ImmutableSet, ) @Immutable @@ -67,6 +68,9 @@ class PhoneSegment(segment: String) : Segment(segment) @Immutable class BechSegment(segment: String) : Segment(segment) +@Immutable +class Base64Segment(segment: String) : Segment(segment) + @Immutable open class HashIndexSegment(segment: String, val hex: String, val extras: String?) : Segment(segment)