- Adds support for AVIF images

- Removes gray border in image urls that couldn't be loaded.
This commit is contained in:
Vitor Pamplona
2024-06-11 10:25:11 -04:00
parent 3a63c1d1ab
commit bd6202f8e1
6 changed files with 62 additions and 43 deletions

View File

@@ -34,7 +34,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.launch 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() val SUPPORTED_VIDEO_FEED_MIME_TYPES_SET = SUPPORTED_VIDEO_FEED_MIME_TYPES.toSet()
object NostrVideoDataSource : NostrDataSource("VideoFeed") { object NostrVideoDataSource : NostrDataSource("VideoFeed") {

View File

@@ -402,6 +402,7 @@ private fun RenderImageOrVideo(
UrlImageView( UrlImageView(
content = content, content = content,
mainImageModifier = mainModifier, mainImageModifier = mainModifier,
loadedImageModifier = Modifier.fillMaxWidth(),
isFiniteHeight = isFiniteHeight, isFiniteHeight = isFiniteHeight,
controllerVisible = controllerVisible, controllerVisible = controllerVisible,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
@@ -436,6 +437,7 @@ private fun RenderImageOrVideo(
LocalImageView( LocalImageView(
content = content, content = content,
mainImageModifier = mainModifier, mainImageModifier = mainModifier,
loadedImageModifier = Modifier.fillMaxWidth(),
isFiniteHeight = isFiniteHeight, isFiniteHeight = isFiniteHeight,
controllerVisible = controllerVisible, controllerVisible = controllerVisible,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,

View File

@@ -117,32 +117,14 @@ fun ZoomableContentView(
// store the dialog open or close state // store the dialog open or close state
var dialogOpen by remember(content) { mutableStateOf(false) } var dialogOpen by remember(content) { mutableStateOf(false) }
var mainImageModifier = val mainImageModifier = Modifier.fillMaxWidth().clickable { dialogOpen = true }
if (roundedCorner) { val loadedImageModifier = if (roundedCorner) MaterialTheme.colorScheme.imageModifier else Modifier.fillMaxWidth()
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 }
}
when (content) { when (content) {
is MediaUrlImage -> is MediaUrlImage ->
SensitivityWarning(content.contentWarning != null, accountViewModel) { SensitivityWarning(content.contentWarning != null, accountViewModel) {
TwoSecondController(content) { controllerVisible -> TwoSecondController(content) { controllerVisible ->
UrlImageView(content, mainImageModifier, isFiniteHeight, controllerVisible, accountViewModel = accountViewModel) UrlImageView(content, mainImageModifier, loadedImageModifier, isFiniteHeight, controllerVisible, accountViewModel = accountViewModel)
} }
} }
is MediaUrlVideo -> is MediaUrlVideo ->
@@ -166,7 +148,7 @@ fun ZoomableContentView(
} }
is MediaLocalImage -> is MediaLocalImage ->
TwoSecondController(content) { controllerVisible -> TwoSecondController(content) { controllerVisible ->
LocalImageView(content, mainImageModifier, isFiniteHeight, controllerVisible, accountViewModel = accountViewModel) LocalImageView(content, mainImageModifier, loadedImageModifier, isFiniteHeight, controllerVisible, accountViewModel = accountViewModel)
} }
is MediaLocalVideo -> is MediaLocalVideo ->
content.localFile?.let { content.localFile?.let {
@@ -215,6 +197,7 @@ fun TwoSecondController(
fun LocalImageView( fun LocalImageView(
content: MediaLocalImage, content: MediaLocalImage,
mainImageModifier: Modifier, mainImageModifier: Modifier,
loadedImageModifier: Modifier,
isFiniteHeight: Boolean, isFiniteHeight: Boolean,
controllerVisible: MutableState<Boolean>, controllerVisible: MutableState<Boolean>,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
@@ -252,14 +235,14 @@ fun LocalImageView(
content.blurhash, content.blurhash,
content.description, content.description,
contentScale, contentScale,
Modifier.aspectRatio(ratio), loadedImageModifier.aspectRatio(ratio),
) )
} else { } else {
DisplayBlurHash( DisplayBlurHash(
content.blurhash, content.blurhash,
content.description, content.description,
contentScale, contentScale,
Modifier, loadedImageModifier,
) )
} }
} else { } else {
@@ -267,10 +250,10 @@ fun LocalImageView(
} }
} }
is AsyncImagePainter.State.Error -> { is AsyncImagePainter.State.Error -> {
BlankNote() BlankNote(loadedImageModifier)
} }
is AsyncImagePainter.State.Success -> { is AsyncImagePainter.State.Success -> {
SubcomposeAsyncImageContent() SubcomposeAsyncImageContent(loadedImageModifier)
content.isVerified?.let { content.isVerified?.let {
AnimatedVisibility( AnimatedVisibility(
@@ -294,7 +277,7 @@ fun LocalImageView(
content.blurhash, content.blurhash,
content.description, content.description,
ContentScale.Crop, ContentScale.Crop,
mainImageModifier loadedImageModifier
.aspectRatio(ratio) .aspectRatio(ratio)
.clickable { showImage.value = true }, .clickable { showImage.value = true },
) )
@@ -310,7 +293,7 @@ fun LocalImageView(
} }
} }
} else { } else {
BlankNote() BlankNote(loadedImageModifier)
} }
} }
@@ -318,6 +301,7 @@ fun LocalImageView(
fun UrlImageView( fun UrlImageView(
content: MediaUrlImage, content: MediaUrlImage,
mainImageModifier: Modifier, mainImageModifier: Modifier,
loadedImageModifier: Modifier,
isFiniteHeight: Boolean, isFiniteHeight: Boolean,
controllerVisible: MutableState<Boolean>, controllerVisible: MutableState<Boolean>,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
@@ -351,14 +335,14 @@ fun UrlImageView(
content.blurhash, content.blurhash,
content.description, content.description,
ContentScale.Crop, ContentScale.Crop,
Modifier.aspectRatio(ratio), loadedImageModifier.aspectRatio(ratio),
) )
} else { } else {
DisplayBlurHash( DisplayBlurHash(
content.blurhash, content.blurhash,
content.description, content.description,
ContentScale.Crop, ContentScale.Crop,
Modifier, loadedImageModifier,
) )
} }
} else { } else {
@@ -369,7 +353,7 @@ fun UrlImageView(
ClickableUrl(urlText = "${content.url} ", url = content.url) ClickableUrl(urlText = "${content.url} ", url = content.url)
} }
is AsyncImagePainter.State.Success -> { is AsyncImagePainter.State.Success -> {
SubcomposeAsyncImageContent() SubcomposeAsyncImageContent(loadedImageModifier)
AnimatedVisibility( AnimatedVisibility(
visible = controllerVisible.value, visible = controllerVisible.value,
@@ -391,7 +375,7 @@ fun UrlImageView(
content.blurhash, content.blurhash,
content.description, content.description,
ContentScale.Crop, ContentScale.Crop,
mainImageModifier loadedImageModifier
.aspectRatio(ratio) .aspectRatio(ratio)
.clickable { showImage.value = true }, .clickable { showImage.value = true },
) )

View File

@@ -688,7 +688,7 @@ class RichTextParserTest {
fun testTextToParse() { fun testTextToParse() {
val state = val state =
RichTextParser() RichTextParser()
.parseText(textToParse, EmptyTagList) .parseText(textToParse, EmptyTagList, null)
org.junit.Assert.assertEquals( 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", "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(", "), state.urlSet.joinToString(", "),
@@ -4037,7 +4037,7 @@ class RichTextParserTest {
fun testShortTextToParse() { fun testShortTextToParse() {
val state = val state =
RichTextParser() 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.urlSet.isEmpty())
org.junit.Assert.assertTrue(state.imagesForPager.isEmpty()) org.junit.Assert.assertTrue(state.imagesForPager.isEmpty())
org.junit.Assert.assertTrue(state.imageList.isEmpty()) org.junit.Assert.assertTrue(state.imageList.isEmpty())
@@ -4051,7 +4051,7 @@ class RichTextParserTest {
@Test @Test
fun testShortNewLinesTextToParse() { fun testShortNewLinesTextToParse() {
val state = 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.urlSet.isEmpty())
org.junit.Assert.assertTrue(state.imagesForPager.isEmpty()) org.junit.Assert.assertTrue(state.imagesForPager.isEmpty())
org.junit.Assert.assertTrue(state.imageList.isEmpty()) org.junit.Assert.assertTrue(state.imageList.isEmpty())
@@ -4076,7 +4076,7 @@ class RichTextParserTest {
val state = val state =
RichTextParser() RichTextParser()
.parseText(text, EmptyTagList) .parseText(text, EmptyTagList, null)
org.junit.Assert.assertEquals( org.junit.Assert.assertEquals(
"https://lnshort.it/live-stream-embeds/", "https://lnshort.it/live-stream-embeds/",
state.urlSet.firstOrNull(), state.urlSet.firstOrNull(),
@@ -4153,7 +4153,7 @@ class RichTextParserTest {
val state = val state =
RichTextParser() RichTextParser()
.parseText(text, EmptyTagList) .parseText(text, EmptyTagList, null)
printStateForDebug(state) printStateForDebug(state)
@@ -4186,7 +4186,7 @@ class RichTextParserTest {
val state = val state =
RichTextParser() RichTextParser()
.parseText(text, EmptyTagList) .parseText(text, EmptyTagList, null)
printStateForDebug(state) 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<String>(
"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 @Test
fun testUrlsEndingInPeriod() { fun testUrlsEndingInPeriod() {
val text = "Thats it! http://vitorpamplona.com/. Thats the note" val text = "Thats it! http://vitorpamplona.com/. Thats the note"
val state = val state =
RichTextParser() RichTextParser()
.parseText(text, EmptyTagList) .parseText(text, EmptyTagList, null)
printStateForDebug(state) printStateForDebug(state)
@@ -4258,7 +4291,7 @@ class RichTextParserTest {
"https://misskey.io/play/9g3qza4jow" "https://misskey.io/play/9g3qza4jow"
val state = val state =
RichTextParser().parseText(text, ImmutableListOfLists(tags)) RichTextParser().parseText(text, ImmutableListOfLists(tags), null)
printStateForDebug(state) printStateForDebug(state)

View File

@@ -318,7 +318,7 @@ class RichTextParser() {
"^((http|https)://)?([A-Za-z0-9-_]+(\\.[A-Za-z0-9-_]+)+)(:[0-9]+)?(/[^?#]*)?(\\?[^#]*)?(#.*)?" "^((http|https)://)?([A-Za-z0-9-_]+(\\.[A-Za-z0-9-_]+)+)(:[0-9]+)?(/[^?#]*)?(\\?[^#]*)?(#.*)?"
.toRegex(RegexOption.IGNORE_CASE) .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 videoExtensions = listOf("mp4", "avi", "wmv", "mpg", "amv", "webm", "mov", "mp3", "m3u8")
val tagIndex = Pattern.compile("\\#\\[([0-9]+)\\](.*)") val tagIndex = Pattern.compile("\\#\\[([0-9]+)\\](.*)")

View File

@@ -88,7 +88,7 @@ class ClassifiedsEvent(
companion object { companion object {
const val KIND = 30402 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" const val ALT = "Classifieds listing"
fun create( fun create(