From bd6202f8e1293be34cc3ce45947e92482987d710 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 11 Jun 2024 10:25:11 -0400 Subject: [PATCH] - Adds support for AVIF images - Removes gray border in image urls that couldn't be loaded. --- .../amethyst/service/NostrVideoDataSource.kt | 2 +- .../ui/components/ZoomableContentDialog.kt | 2 + .../ui/components/ZoomableContentView.kt | 48 ++++++------------ .../commons/richtext/RichTextParserTest.kt | 49 ++++++++++++++++--- .../commons/richtext/RichTextParser.kt | 2 +- .../quartz/events/ClassifiedsEvent.kt | 2 +- 6 files changed, 62 insertions(+), 43 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrVideoDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrVideoDataSource.kt index 1265454ce..9cca2cfd2 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrVideoDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrVideoDataSource.kt @@ -34,7 +34,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch -val SUPPORTED_VIDEO_FEED_MIME_TYPES = listOf("image/jpeg", "image/gif", "image/png", "image/webp", "video/mp4", "video/mpeg", "video/webm", "audio/aac", "audio/mpeg", "audio/webm", "audio/wav") +val SUPPORTED_VIDEO_FEED_MIME_TYPES = listOf("image/jpeg", "image/gif", "image/png", "image/webp", "video/mp4", "video/mpeg", "video/webm", "audio/aac", "audio/mpeg", "audio/webm", "audio/wav", "image/avif") val SUPPORTED_VIDEO_FEED_MIME_TYPES_SET = SUPPORTED_VIDEO_FEED_MIME_TYPES.toSet() object NostrVideoDataSource : NostrDataSource("VideoFeed") { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentDialog.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentDialog.kt index 147b86f9b..3025f957e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentDialog.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentDialog.kt @@ -402,6 +402,7 @@ private fun RenderImageOrVideo( UrlImageView( content = content, mainImageModifier = mainModifier, + loadedImageModifier = Modifier.fillMaxWidth(), isFiniteHeight = isFiniteHeight, controllerVisible = controllerVisible, accountViewModel = accountViewModel, @@ -436,6 +437,7 @@ private fun RenderImageOrVideo( LocalImageView( content = content, mainImageModifier = mainModifier, + loadedImageModifier = Modifier.fillMaxWidth(), isFiniteHeight = isFiniteHeight, controllerVisible = controllerVisible, accountViewModel = accountViewModel, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt index 68cbf9944..00f0f5651 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt @@ -117,32 +117,14 @@ fun ZoomableContentView( // store the dialog open or close state var dialogOpen by remember(content) { mutableStateOf(false) } - var mainImageModifier = - if (roundedCorner) { - MaterialTheme.colorScheme.imageModifier - } else { - Modifier.fillMaxWidth() - } - - if (content is MediaUrlContent) { - mainImageModifier = - mainImageModifier.clickable( - onClick = { dialogOpen = true }, - ) - } else if (content is MediaPreloadedContent) { - mainImageModifier = - mainImageModifier.clickable( - onClick = { dialogOpen = true }, - ) - } else { - mainImageModifier = mainImageModifier.clickable { dialogOpen = true } - } + val mainImageModifier = Modifier.fillMaxWidth().clickable { dialogOpen = true } + val loadedImageModifier = if (roundedCorner) MaterialTheme.colorScheme.imageModifier else Modifier.fillMaxWidth() when (content) { is MediaUrlImage -> SensitivityWarning(content.contentWarning != null, accountViewModel) { TwoSecondController(content) { controllerVisible -> - UrlImageView(content, mainImageModifier, isFiniteHeight, controllerVisible, accountViewModel = accountViewModel) + UrlImageView(content, mainImageModifier, loadedImageModifier, isFiniteHeight, controllerVisible, accountViewModel = accountViewModel) } } is MediaUrlVideo -> @@ -166,7 +148,7 @@ fun ZoomableContentView( } is MediaLocalImage -> TwoSecondController(content) { controllerVisible -> - LocalImageView(content, mainImageModifier, isFiniteHeight, controllerVisible, accountViewModel = accountViewModel) + LocalImageView(content, mainImageModifier, loadedImageModifier, isFiniteHeight, controllerVisible, accountViewModel = accountViewModel) } is MediaLocalVideo -> content.localFile?.let { @@ -215,6 +197,7 @@ fun TwoSecondController( fun LocalImageView( content: MediaLocalImage, mainImageModifier: Modifier, + loadedImageModifier: Modifier, isFiniteHeight: Boolean, controllerVisible: MutableState, accountViewModel: AccountViewModel, @@ -252,14 +235,14 @@ fun LocalImageView( content.blurhash, content.description, contentScale, - Modifier.aspectRatio(ratio), + loadedImageModifier.aspectRatio(ratio), ) } else { DisplayBlurHash( content.blurhash, content.description, contentScale, - Modifier, + loadedImageModifier, ) } } else { @@ -267,10 +250,10 @@ fun LocalImageView( } } is AsyncImagePainter.State.Error -> { - BlankNote() + BlankNote(loadedImageModifier) } is AsyncImagePainter.State.Success -> { - SubcomposeAsyncImageContent() + SubcomposeAsyncImageContent(loadedImageModifier) content.isVerified?.let { AnimatedVisibility( @@ -294,7 +277,7 @@ fun LocalImageView( content.blurhash, content.description, ContentScale.Crop, - mainImageModifier + loadedImageModifier .aspectRatio(ratio) .clickable { showImage.value = true }, ) @@ -310,7 +293,7 @@ fun LocalImageView( } } } else { - BlankNote() + BlankNote(loadedImageModifier) } } @@ -318,6 +301,7 @@ fun LocalImageView( fun UrlImageView( content: MediaUrlImage, mainImageModifier: Modifier, + loadedImageModifier: Modifier, isFiniteHeight: Boolean, controllerVisible: MutableState, accountViewModel: AccountViewModel, @@ -351,14 +335,14 @@ fun UrlImageView( content.blurhash, content.description, ContentScale.Crop, - Modifier.aspectRatio(ratio), + loadedImageModifier.aspectRatio(ratio), ) } else { DisplayBlurHash( content.blurhash, content.description, ContentScale.Crop, - Modifier, + loadedImageModifier, ) } } else { @@ -369,7 +353,7 @@ fun UrlImageView( ClickableUrl(urlText = "${content.url} ", url = content.url) } is AsyncImagePainter.State.Success -> { - SubcomposeAsyncImageContent() + SubcomposeAsyncImageContent(loadedImageModifier) AnimatedVisibility( visible = controllerVisible.value, @@ -391,7 +375,7 @@ fun UrlImageView( content.blurhash, content.description, ContentScale.Crop, - mainImageModifier + loadedImageModifier .aspectRatio(ratio) .clickable { showImage.value = true }, ) diff --git a/commons/src/androidTest/java/com/vitorpamplona/amethyst/commons/richtext/RichTextParserTest.kt b/commons/src/androidTest/java/com/vitorpamplona/amethyst/commons/richtext/RichTextParserTest.kt index bf98efcc3..a940c9ca7 100644 --- a/commons/src/androidTest/java/com/vitorpamplona/amethyst/commons/richtext/RichTextParserTest.kt +++ b/commons/src/androidTest/java/com/vitorpamplona/amethyst/commons/richtext/RichTextParserTest.kt @@ -688,7 +688,7 @@ class RichTextParserTest { fun testTextToParse() { val state = RichTextParser() - .parseText(textToParse, EmptyTagList) + .parseText(textToParse, EmptyTagList, null) org.junit.Assert.assertEquals( "relay.shitforce.one, relayable.org, universe.nostrich.land, nos.lol, universe.nostrich.land?lang=zh, universe.nostrich.land?lang=en, relay.damus.io, relay.nostr.wirednet.jp, offchain.pub, nostr.rocks, relay.wellorder.net, nostr.oxtr.dev, universe.nostrich.land?lang=ja, relay.mostr.pub, nostr.bitcoiner.social, Nostr-Check.com, MR.Rabbit, Ancap.su, ⚡\uFE0Fsatscoinsv@getalby.com, miceliomad@miceliomad.github.io/nostr/, zapper.lol, smies.me, baller.hodl", state.urlSet.joinToString(", "), @@ -4037,7 +4037,7 @@ class RichTextParserTest { fun testShortTextToParse() { val state = RichTextParser() - .parseText("Hi, how are you doing? ", EmptyTagList) + .parseText("Hi, how are you doing? ", EmptyTagList, null) org.junit.Assert.assertTrue(state.urlSet.isEmpty()) org.junit.Assert.assertTrue(state.imagesForPager.isEmpty()) org.junit.Assert.assertTrue(state.imageList.isEmpty()) @@ -4051,7 +4051,7 @@ class RichTextParserTest { @Test fun testShortNewLinesTextToParse() { val state = - RichTextParser().parseText("\nHi, \nhow\n\n\n are you doing? \n", EmptyTagList) + RichTextParser().parseText("\nHi, \nhow\n\n\n are you doing? \n", EmptyTagList, null) org.junit.Assert.assertTrue(state.urlSet.isEmpty()) org.junit.Assert.assertTrue(state.imagesForPager.isEmpty()) org.junit.Assert.assertTrue(state.imageList.isEmpty()) @@ -4076,7 +4076,7 @@ class RichTextParserTest { val state = RichTextParser() - .parseText(text, EmptyTagList) + .parseText(text, EmptyTagList, null) org.junit.Assert.assertEquals( "https://lnshort.it/live-stream-embeds/", state.urlSet.firstOrNull(), @@ -4153,7 +4153,7 @@ class RichTextParserTest { val state = RichTextParser() - .parseText(text, EmptyTagList) + .parseText(text, EmptyTagList, null) printStateForDebug(state) @@ -4186,7 +4186,7 @@ class RichTextParserTest { val state = RichTextParser() - .parseText(text, EmptyTagList) + .parseText(text, EmptyTagList, null) printStateForDebug(state) @@ -4211,13 +4211,46 @@ class RichTextParserTest { } } + @Test + fun testAVif() { + val text = + "Goon Night everybody :sleep:\n" + + "81ca16-b665-4f57-80cb-11a58461fb61.avif\n" + + "\n" + + "https://bae.st/media/66b08dde784287ed8f92c455bc62076a04671ccb44097550626a532185a5d3ed.avif?name=81ca16-b665-4f57-80cb-11a58461fb61.avif" + + val state = + RichTextParser() + .parseText(text, EmptyTagList, null) + + printStateForDebug(state) + + val expectedResult = + listOf( + "RegularText(Goon Night everybody :sleep:)", + "Image(81ca16-b665-4f57-80cb-11a58461fb61.avif)", + "RegularText()", + "Image(https://bae.st/media/66b08dde784287ed8f92c455bc62076a04671ccb44097550626a532185a5d3ed.avif?name=81ca16-b665-4f57-80cb-11a58461fb61.avif)", + ) + + state.paragraphs + .map { it.words } + .flatten() + .forEachIndexed { index, seg -> + org.junit.Assert.assertEquals( + expectedResult[index], + "${seg.javaClass.simpleName.replace("Segment", "")}(${seg.segmentText})", + ) + } + } + @Test fun testUrlsEndingInPeriod() { val text = "That’s it! http://vitorpamplona.com/. That’s the note" val state = RichTextParser() - .parseText(text, EmptyTagList) + .parseText(text, EmptyTagList, null) printStateForDebug(state) @@ -4258,7 +4291,7 @@ class RichTextParserTest { "https://misskey.io/play/9g3qza4jow" val state = - RichTextParser().parseText(text, ImmutableListOfLists(tags)) + RichTextParser().parseText(text, ImmutableListOfLists(tags), null) printStateForDebug(state) 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 772b4e71f..6d97e120c 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 @@ -318,7 +318,7 @@ class RichTextParser() { "^((http|https)://)?([A-Za-z0-9-_]+(\\.[A-Za-z0-9-_]+)+)(:[0-9]+)?(/[^?#]*)?(\\?[^#]*)?(#.*)?" .toRegex(RegexOption.IGNORE_CASE) - val imageExtensions = listOf("png", "jpg", "gif", "bmp", "jpeg", "webp", "svg") + val imageExtensions = listOf("png", "jpg", "gif", "bmp", "jpeg", "webp", "svg", "avif") val videoExtensions = listOf("mp4", "avi", "wmv", "mpg", "amv", "webm", "mov", "mp3", "m3u8") val tagIndex = Pattern.compile("\\#\\[([0-9]+)\\](.*)") diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/ClassifiedsEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/ClassifiedsEvent.kt index 929ead011..dbc6a0296 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/ClassifiedsEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/ClassifiedsEvent.kt @@ -88,7 +88,7 @@ class ClassifiedsEvent( companion object { const val KIND = 30402 - private val imageExtensions = listOf("png", "jpg", "gif", "bmp", "jpeg", "webp", "svg") + private val imageExtensions = listOf("png", "jpg", "gif", "bmp", "jpeg", "webp", "svg", "avif") const val ALT = "Classifieds listing" fun create(