refactor(feeds): unify content warning behind ContentWarningGate

Collapse the per-media, per-blurhash and grid-of-blurhash variants into a
single ContentWarningGate that takes a backdrop slot and a sizing
modifier. Callers choose the backdrop (single blurhash, grid of
blurhashes, or none).

Remove the isSensitive field from MediaUrl{Image,Video}; it was a
rendering concern leaked into a media model. Sensitivity is now only
expressed to the gate. Picture/Video feeds and their display variants
compute isSensitive + reasons themselves and wrap with the gate.

ZoomableContentView keeps a default internal gate (fires when
content.contentWarning != null) so the 14 callers that don't need
grid-level handling still get the blurhash overlay automatically.

Also rename collectPictureReasons to collectContentWarningReasons.
This commit is contained in:
Claude
2026-04-18 23:58:36 +00:00
parent d34b5702a5
commit 60a56440d1
8 changed files with 202 additions and 223 deletions
@@ -117,15 +117,13 @@ fun SensitivityWarning(
}
@Composable
fun SensitivityWarningOverBlurhash(
fun ContentWarningGate(
isSensitive: Boolean,
reasons: Set<String>,
blurhash: String?,
ratio: Float?,
description: String?,
contentScale: ContentScale,
preloadUrl: String?,
preloadUrls: List<String>,
accountViewModel: AccountViewModel,
modifier: Modifier = Modifier.fillMaxWidth(),
backdrop: (@Composable () -> Unit)? = null,
content: @Composable () -> Unit,
) {
if (!isSensitive) {
@@ -137,51 +135,12 @@ fun SensitivityWarningOverBlurhash(
var showContentWarningNote by remember(accountState) { mutableStateOf(accountState.value != true) }
if (showContentWarningNote && preloadUrl != null) {
if (showContentWarningNote && preloadUrls.isNotEmpty()) {
val context = LocalContext.current
LaunchedEffect(preloadUrl) {
runCatching {
context.imageLoader.enqueue(ImageRequest.Builder(context).data(preloadUrl).build())
}
}
}
CrossfadeIfEnabled(targetState = showContentWarningNote, accountViewModel = accountViewModel) {
if (it) {
if (blurhash != null) {
ContentWarningOverBlurhash(reasons, blurhash, ratio, description, contentScale) { showContentWarningNote = false }
} else {
ContentWarningNote(reasons.firstOrNull()) { showContentWarningNote = false }
}
} else {
content()
}
}
}
@Composable
fun SensitivityWarningOverBlurhashGrid(
isSensitive: Boolean,
reasons: Set<String>,
media: List<MediaUrlImage>,
accountViewModel: AccountViewModel,
content: @Composable () -> Unit,
) {
if (!isSensitive || media.isEmpty()) {
content()
return
}
val accountState = accountViewModel.showSensitiveContent().collectAsStateWithLifecycle()
var showContentWarningNote by remember(accountState) { mutableStateOf(accountState.value != true) }
if (showContentWarningNote) {
val context = LocalContext.current
LaunchedEffect(media) {
media.forEach { item ->
LaunchedEffect(preloadUrls) {
preloadUrls.forEach { url ->
runCatching {
context.imageLoader.enqueue(ImageRequest.Builder(context).data(item.url).build())
context.imageLoader.enqueue(ImageRequest.Builder(context).data(url).build())
}
}
}
@@ -189,7 +148,14 @@ fun SensitivityWarningOverBlurhashGrid(
CrossfadeIfEnabled(targetState = showContentWarningNote, accountViewModel = accountViewModel) {
if (it) {
ContentWarningOverBlurhashGrid(reasons, media) { showContentWarningNote = false }
if (backdrop != null) {
Box(modifier = modifier.clipToBounds()) {
backdrop()
ContentWarningOverlayBody(reasons) { showContentWarningNote = false }
}
} else {
ContentWarningNote(reasons.firstOrNull()) { showContentWarningNote = false }
}
} else {
content()
}
@@ -239,85 +205,44 @@ fun ContentWarningNoteWithBigReasonPreview() {
}
}
@Preview
@Composable
fun ContentWarningOverBlurhashPreview() {
ThemeComparisonColumn {
ContentWarningOverBlurhash(
reasons = setOf("Spoilers"),
blurhash = "LEHV6nWB2yk8pyo0adR*.7kCMdnj",
ratio = 16f / 9f,
description = null,
contentScale = ContentScale.FillWidth,
onDismiss = {},
)
}
}
@Preview
@Composable
fun ContentWarningOverBlurhashMultiReasonPreview() {
ThemeComparisonColumn {
ContentWarningOverBlurhash(
reasons = setOf("Spoilers", "Violence", "Nudity"),
blurhash = "LEHV6nWB2yk8pyo0adR*.7kCMdnj",
ratio = 16f / 9f,
description = null,
contentScale = ContentScale.FillWidth,
onDismiss = {},
)
}
}
@Composable
fun ContentWarningOverBlurhash(
reasons: Set<String>,
fun BlurhashBackdrop(
blurhash: String,
ratio: Float?,
description: String?,
contentScale: ContentScale,
onDismiss: () -> Unit,
) {
val sizingModifier =
when {
contentScale == ContentScale.Crop -> Modifier.fillMaxSize()
ratio != null -> Modifier.fillMaxWidth().aspectRatio(ratio)
else -> Modifier.fillMaxWidth()
}
Box(modifier = sizingModifier.clipToBounds()) {
DisplayBlurHash(
blurhash = blurhash,
description = description,
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxSize(),
)
ContentWarningOverlayBody(reasons, onDismiss)
}
DisplayBlurHash(
blurhash = blurhash,
description = description,
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxSize(),
)
}
@Composable
fun ContentWarningOverBlurhashGrid(
reasons: Set<String>,
media: List<MediaUrlImage>,
onDismiss: () -> Unit,
) {
Box(modifier = Modifier.fillMaxWidth().aspectRatio(1f).clipToBounds()) {
AutoNonlazyGrid(media.size) { idx ->
val item = media[idx]
if (item.blurhash != null) {
DisplayBlurHash(
blurhash = item.blurhash,
description = item.description,
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxSize(),
)
}
fun BlurhashGridBackdrop(media: List<MediaUrlImage>) {
AutoNonlazyGrid(media.size) { idx ->
val item = media[idx]
if (item.blurhash != null) {
DisplayBlurHash(
blurhash = item.blurhash,
description = item.description,
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxSize(),
)
}
ContentWarningOverlayBody(reasons, onDismiss)
}
}
fun mediaSizingModifier(
ratio: Float?,
contentScale: ContentScale,
): Modifier =
when {
contentScale == ContentScale.Crop -> Modifier.fillMaxSize()
ratio != null -> Modifier.fillMaxWidth().aspectRatio(ratio)
else -> Modifier.fillMaxWidth()
}
@Composable
private fun ContentWarningOverlayBody(
reasons: Set<String>,
@@ -487,7 +412,7 @@ fun ContentWarningNote(
}
}
fun collectPictureReasons(event: Event): Set<String> {
fun collectContentWarningReasons(event: Event): Set<String> {
val reasons = linkedSetOf<String>()
event.contentWarningReason()?.takeIf { it.isNotBlank() }?.let { reasons.add(it) }
event.imetas().forEach { iMeta ->
@@ -151,15 +151,13 @@ fun ZoomableContentView(
when (content) {
is MediaUrlImage -> {
val ratio = content.dim?.aspectRatio() ?: MediaAspectRatioCache.get(content.url)
SensitivityWarningOverBlurhash(
isSensitive = content.isSensitive,
ContentWarningGate(
isSensitive = content.contentWarning != null,
reasons = setOfNotNull(content.contentWarning),
blurhash = content.blurhash,
ratio = ratio,
description = content.description,
contentScale = contentScale,
preloadUrl = content.url,
preloadUrls = listOf(content.url),
accountViewModel = accountViewModel,
modifier = mediaSizingModifier(ratio, contentScale),
backdrop = content.blurhash?.let { blurhash -> { BlurhashBackdrop(blurhash, content.description) } },
) {
TwoSecondController(content) { controllerVisible ->
val mainImageModifier =
@@ -175,15 +173,13 @@ fun ZoomableContentView(
is MediaUrlVideo -> {
val ratio = content.dim?.aspectRatio() ?: MediaAspectRatioCache.get(content.url)
SensitivityWarningOverBlurhash(
isSensitive = content.isSensitive,
ContentWarningGate(
isSensitive = content.contentWarning != null,
reasons = setOfNotNull(content.contentWarning),
blurhash = content.blurhash,
ratio = ratio,
description = content.description,
contentScale = contentScale,
preloadUrl = null,
preloadUrls = emptyList(),
accountViewModel = accountViewModel,
modifier = mediaSizingModifier(ratio, contentScale),
backdrop = content.blurhash?.let { blurhash -> { BlurhashBackdrop(blurhash, content.description) } },
) {
Box(
modifier = Modifier.fillMaxWidth().then(boundsTrackingModifier),
@@ -23,6 +23,8 @@ package com.vitorpamplona.amethyst.ui.note.types
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
@@ -37,12 +39,16 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.style.TextOverflow
import com.vitorpamplona.amethyst.commons.model.EmptyTagList
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage
import com.vitorpamplona.amethyst.model.MediaAspectRatioCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.components.AutoNonlazyGrid
import com.vitorpamplona.amethyst.ui.components.SensitivityWarningOverBlurhashGrid
import com.vitorpamplona.amethyst.ui.components.BlurhashBackdrop
import com.vitorpamplona.amethyst.ui.components.BlurhashGridBackdrop
import com.vitorpamplona.amethyst.ui.components.ContentWarningGate
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
import com.vitorpamplona.amethyst.ui.components.ZoomableContentView
import com.vitorpamplona.amethyst.ui.components.collectPictureReasons
import com.vitorpamplona.amethyst.ui.components.collectContentWarningReasons
import com.vitorpamplona.amethyst.ui.components.mediaSizingModifier
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
@@ -63,8 +69,7 @@ fun PictureDisplay(
val event = (note.event as? PictureEvent) ?: return
val uri = note.toNostrUri()
val isSensitive = event.isSensitiveOrNSFW()
val reasons = collectPictureReasons(event)
val inGrid = event.imetaTags().size > 1
val reasons = collectContentWarningReasons(event)
val images by
remember(note) {
@@ -79,8 +84,6 @@ fun PictureDisplay(
blurhash = it.blurhash,
dim = it.dimension,
uri = uri,
contentWarning = reasons.firstOrNull(),
isSensitive = isSensitive && !inGrid,
mimeType = it.mimeType,
)
}.toImmutableList(),
@@ -106,19 +109,31 @@ fun PictureDisplay(
}
if (images.size == 1) {
ZoomableContentView(
content = images.first(),
images = images,
roundedCorner = roundedCorner,
contentScale = ContentScale.FillWidth,
accountViewModel = accountViewModel,
)
} else {
SensitivityWarningOverBlurhashGrid(
val ratio = first.dim?.aspectRatio() ?: MediaAspectRatioCache.get(first.url)
ContentWarningGate(
isSensitive = isSensitive,
reasons = reasons,
media = images,
preloadUrls = listOf(first.url),
accountViewModel = accountViewModel,
modifier = mediaSizingModifier(ratio, ContentScale.FillWidth),
backdrop = first.blurhash?.let { blurhash -> { BlurhashBackdrop(blurhash, first.description) } },
) {
ZoomableContentView(
content = first,
images = images,
roundedCorner = roundedCorner,
contentScale = ContentScale.FillWidth,
accountViewModel = accountViewModel,
)
}
} else {
ContentWarningGate(
isSensitive = isSensitive,
reasons = reasons,
preloadUrls = images.map { it.url },
accountViewModel = accountViewModel,
modifier = Modifier.fillMaxWidth().aspectRatio(1f),
backdrop = { BlurhashGridBackdrop(images) },
) {
AutoNonlazyGrid(images.size) {
ZoomableContentView(
@@ -29,12 +29,16 @@ import com.vitorpamplona.amethyst.commons.richtext.BaseMediaContent
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlVideo
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
import com.vitorpamplona.amethyst.model.MediaAspectRatioCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.components.BlurhashBackdrop
import com.vitorpamplona.amethyst.ui.components.ContentWarningGate
import com.vitorpamplona.amethyst.ui.components.ZoomableContentView
import com.vitorpamplona.amethyst.ui.components.collectContentWarningReasons
import com.vitorpamplona.amethyst.ui.components.mediaSizingModifier
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip31Alts.alt
import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarningReason
import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitiveOrNSFW
import com.vitorpamplona.quartz.nip71Video.VideoEvent
@@ -49,13 +53,13 @@ fun JustVideoDisplay(
val event = (videoEvent as? Event) ?: return
val imeta = videoEvent.imetaTags().getOrNull(0) ?: return
val isSensitive = event.isSensitiveOrNSFW()
val reasons = collectContentWarningReasons(event)
val isImage = imeta.mimeType?.startsWith("image/") == true || RichTextParser.isImageUrl(imeta.url)
val content by
remember(note) {
val description = event.content.ifEmpty { null } ?: imeta.alt ?: event.alt()
val isImage = imeta.mimeType?.startsWith("image/") == true || RichTextParser.isImageUrl(imeta.url)
val isSensitive = event.isSensitiveOrNSFW()
val contentWarning = event.contentWarningReason()
mutableStateOf<BaseMediaContent>(
if (isImage) {
@@ -66,8 +70,6 @@ fun JustVideoDisplay(
blurhash = imeta.blurhash,
dim = imeta.dimension,
uri = note.toNostrUri(),
contentWarning = contentWarning,
isSensitive = isSensitive,
mimeType = imeta.mimeType,
)
} else {
@@ -79,18 +81,27 @@ fun JustVideoDisplay(
dim = imeta.dimension,
uri = note.toNostrUri(),
authorName = note.author?.toBestDisplayName(),
contentWarning = contentWarning,
isSensitive = isSensitive,
mimeType = imeta.mimeType,
)
},
)
}
ZoomableContentView(
content = content,
roundedCorner = roundedCorner,
contentScale = contentScale,
val ratio = imeta.dimension?.aspectRatio() ?: MediaAspectRatioCache.get(imeta.url)
ContentWarningGate(
isSensitive = isSensitive,
reasons = reasons,
preloadUrls = if (isImage) listOf(imeta.url) else emptyList(),
accountViewModel = accountViewModel,
)
modifier = mediaSizingModifier(ratio, contentScale),
backdrop = imeta.blurhash?.let { blurhash -> { BlurhashBackdrop(blurhash, content.description) } },
) {
ZoomableContentView(
content = content,
roundedCorner = roundedCorner,
contentScale = contentScale,
accountViewModel = accountViewModel,
)
}
}
@@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.pictures
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
@@ -39,11 +40,15 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage
import com.vitorpamplona.amethyst.model.MediaAspectRatioCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.components.AutoNonlazyGrid
import com.vitorpamplona.amethyst.ui.components.SensitivityWarningOverBlurhashGrid
import com.vitorpamplona.amethyst.ui.components.BlurhashBackdrop
import com.vitorpamplona.amethyst.ui.components.BlurhashGridBackdrop
import com.vitorpamplona.amethyst.ui.components.ContentWarningGate
import com.vitorpamplona.amethyst.ui.components.ZoomableContentView
import com.vitorpamplona.amethyst.ui.components.collectPictureReasons
import com.vitorpamplona.amethyst.ui.components.collectContentWarningReasons
import com.vitorpamplona.amethyst.ui.components.mediaSizingModifier
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.note.ReactionsRow
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -94,8 +99,7 @@ private fun PictureCardImage(
) {
val uri = note.toNostrUri()
val isSensitive = event.isSensitiveOrNSFW()
val reasons = collectPictureReasons(event)
val inGrid = event.imetaTags().size > 1
val reasons = collectContentWarningReasons(event)
val images by
remember(note) {
@@ -110,41 +114,50 @@ private fun PictureCardImage(
blurhash = it.blurhash,
dim = it.dimension,
uri = uri,
contentWarning = reasons.firstOrNull(),
// In grid mode the warning is handled at the grid level to avoid
// one overlay per image. Single-image posts still warn per media.
isSensitive = isSensitive && !inGrid,
mimeType = it.mimeType,
)
}.toImmutableList(),
)
}
if (images.isNotEmpty()) {
if (images.size == 1) {
if (images.isEmpty()) return
if (images.size == 1) {
val single = images.first()
val ratio = single.dim?.aspectRatio() ?: MediaAspectRatioCache.get(single.url)
ContentWarningGate(
isSensitive = isSensitive,
reasons = reasons,
preloadUrls = listOf(single.url),
accountViewModel = accountViewModel,
modifier = mediaSizingModifier(ratio, ContentScale.FillWidth),
backdrop = single.blurhash?.let { blurhash -> { BlurhashBackdrop(blurhash, single.description) } },
) {
ZoomableContentView(
content = images.first(),
content = single,
images = images,
roundedCorner = false,
contentScale = ContentScale.FillWidth,
accountViewModel = accountViewModel,
)
} else {
SensitivityWarningOverBlurhashGrid(
isSensitive = isSensitive,
reasons = reasons,
media = images,
accountViewModel = accountViewModel,
) {
AutoNonlazyGrid(images.size) {
ZoomableContentView(
content = images[it],
images = images,
roundedCorner = false,
contentScale = ContentScale.Crop,
accountViewModel = accountViewModel,
)
}
}
} else {
ContentWarningGate(
isSensitive = isSensitive,
reasons = reasons,
preloadUrls = images.map { it.url },
accountViewModel = accountViewModel,
modifier = Modifier.fillMaxWidth().aspectRatio(1f),
backdrop = { BlurhashGridBackdrop(images) },
) {
AutoNonlazyGrid(images.size) {
ZoomableContentView(
content = images[it],
images = images,
roundedCorner = false,
contentScale = ContentScale.Crop,
accountViewModel = accountViewModel,
)
}
}
}
@@ -42,15 +42,19 @@ import com.vitorpamplona.amethyst.commons.richtext.BaseMediaContent
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlVideo
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
import com.vitorpamplona.amethyst.model.MediaAspectRatioCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.components.BlurhashBackdrop
import com.vitorpamplona.amethyst.ui.components.ContentWarningGate
import com.vitorpamplona.amethyst.ui.components.ZoomableContentView
import com.vitorpamplona.amethyst.ui.components.collectContentWarningReasons
import com.vitorpamplona.amethyst.ui.components.mediaSizingModifier
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.note.ReactionsRow
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.UserCardHeader
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip31Alts.alt
import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarningReason
import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitiveOrNSFW
import com.vitorpamplona.quartz.nip71Video.VideoEvent
import kotlin.text.ifEmpty
@@ -99,13 +103,13 @@ private fun VideoCardImage(
val event = (event as? Event) ?: return
val imeta = videoEvent.imetaTags().getOrNull(0) ?: return
val isSensitive = event.isSensitiveOrNSFW()
val reasons = collectContentWarningReasons(event)
val isImage = imeta.mimeType?.startsWith("image/") == true || RichTextParser.isImageUrl(imeta.url)
val content by
remember(note) {
val description = event.content.ifEmpty { null } ?: imeta.alt ?: event.alt()
val isImage = imeta.mimeType?.startsWith("image/") == true || RichTextParser.isImageUrl(imeta.url)
val isSensitive = event.isSensitiveOrNSFW()
val contentWarning = event.contentWarningReason()
mutableStateOf<BaseMediaContent>(
if (isImage) {
@@ -116,8 +120,6 @@ private fun VideoCardImage(
blurhash = imeta.blurhash,
dim = imeta.dimension,
uri = note.toNostrUri(),
contentWarning = contentWarning,
isSensitive = isSensitive,
mimeType = imeta.mimeType,
)
} else {
@@ -129,20 +131,29 @@ private fun VideoCardImage(
dim = imeta.dimension,
uri = note.toNostrUri(),
authorName = note.author?.toBestDisplayName(),
contentWarning = contentWarning,
isSensitive = isSensitive,
mimeType = imeta.mimeType,
)
},
)
}
ZoomableContentView(
content = content,
roundedCorner = false,
contentScale = ContentScale.FillWidth,
val ratio = imeta.dimension?.aspectRatio() ?: MediaAspectRatioCache.get(imeta.url)
ContentWarningGate(
isSensitive = isSensitive,
reasons = reasons,
preloadUrls = if (isImage) listOf(imeta.url) else emptyList(),
accountViewModel = accountViewModel,
)
modifier = mediaSizingModifier(ratio, ContentScale.FillWidth),
backdrop = imeta.blurhash?.let { blurhash -> { BlurhashBackdrop(blurhash, content.description) } },
) {
ZoomableContentView(
content = content,
roundedCorner = false,
contentScale = ContentScale.FillWidth,
accountViewModel = accountViewModel,
)
}
}
@Composable
@@ -42,14 +42,18 @@ import com.vitorpamplona.amethyst.commons.richtext.BaseMediaContent
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlVideo
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
import com.vitorpamplona.amethyst.model.MediaAspectRatioCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.components.BlurhashBackdrop
import com.vitorpamplona.amethyst.ui.components.ContentWarningGate
import com.vitorpamplona.amethyst.ui.components.ZoomableContentView
import com.vitorpamplona.amethyst.ui.components.collectContentWarningReasons
import com.vitorpamplona.amethyst.ui.components.mediaSizingModifier
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.note.ReactionsRow
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip31Alts.alt
import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarningReason
import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitiveOrNSFW
import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent
import kotlin.text.ifEmpty
@@ -96,16 +100,17 @@ private fun FileHeaderCardImage(
) {
val fullUrl = event.url() ?: return
val isSensitive = event.isSensitiveOrNSFW()
val reasons = collectContentWarningReasons(event)
val isImage = event.mimeType()?.startsWith("image/") == true || RichTextParser.isImageUrl(fullUrl)
val blurHash = event.blurhash()
val dimensions = event.dimensions()
val content by remember(note) {
val blurHash = event.blurhash()
val hash = event.hash()
val dimensions = event.dimensions()
val description = event.content.ifEmpty { null } ?: event.alt()
val isImage = event.mimeType()?.startsWith("image/") == true || RichTextParser.isImageUrl(fullUrl)
val uri = note.toNostrUri()
val mimeType = event.mimeType()
val isSensitive = event.isSensitiveOrNSFW()
val contentWarning = event.contentWarningReason()
mutableStateOf<BaseMediaContent>(
if (isImage) {
@@ -116,8 +121,6 @@ private fun FileHeaderCardImage(
blurhash = blurHash,
dim = dimensions,
uri = uri,
contentWarning = contentWarning,
isSensitive = isSensitive,
mimeType = mimeType,
)
} else {
@@ -129,20 +132,29 @@ private fun FileHeaderCardImage(
dim = dimensions,
uri = uri,
authorName = note.author?.toBestDisplayName(),
contentWarning = contentWarning,
isSensitive = isSensitive,
mimeType = mimeType,
)
},
)
}
ZoomableContentView(
content = content,
roundedCorner = false,
contentScale = ContentScale.FillWidth,
val ratio = dimensions?.aspectRatio() ?: MediaAspectRatioCache.get(fullUrl)
ContentWarningGate(
isSensitive = isSensitive,
reasons = reasons,
preloadUrls = if (isImage) listOf(fullUrl) else emptyList(),
accountViewModel = accountViewModel,
)
modifier = mediaSizingModifier(ratio, ContentScale.FillWidth),
backdrop = blurHash?.let { blurhash -> { BlurhashBackdrop(blurhash, content.description) } },
) {
ZoomableContentView(
content = content,
roundedCorner = false,
contentScale = ContentScale.FillWidth,
accountViewModel = accountViewModel,
)
}
}
@Composable
@@ -51,7 +51,6 @@ open class MediaUrlImage(
dim: DimensionTag? = null,
uri: String? = null,
val contentWarning: String? = null,
val isSensitive: Boolean = contentWarning != null,
mimeType: String? = null,
) : MediaUrlContent(url, description, hash, dim, blurhash, uri, mimeType)
@@ -63,12 +62,11 @@ class EncryptedMediaUrlImage(
dim: DimensionTag? = null,
uri: String? = null,
contentWarning: String? = null,
isSensitive: Boolean = contentWarning != null,
mimeType: String? = null,
val encryptionAlgo: String,
val encryptionKey: ByteArray,
val encryptionNonce: ByteArray,
) : MediaUrlImage(url, description, hash, blurhash, dim, uri, contentWarning, isSensitive, mimeType)
) : MediaUrlImage(url, description, hash, blurhash, dim, uri, contentWarning, mimeType)
@Immutable
open class MediaUrlVideo(
@@ -81,7 +79,6 @@ open class MediaUrlVideo(
val authorName: String? = null,
blurhash: String? = null,
val contentWarning: String? = null,
val isSensitive: Boolean = contentWarning != null,
mimeType: String? = null,
) : MediaUrlContent(url, description, hash, dim, blurhash, uri, mimeType)
@@ -96,12 +93,11 @@ class EncryptedMediaUrlVideo(
authorName: String? = null,
blurhash: String? = null,
contentWarning: String? = null,
isSensitive: Boolean = contentWarning != null,
mimeType: String? = null,
val encryptionAlgo: String,
val encryptionKey: ByteArray,
val encryptionNonce: ByteArray,
) : MediaUrlVideo(url, description, hash, dim, uri, artworkUri, authorName, blurhash, contentWarning, isSensitive, mimeType)
) : MediaUrlVideo(url, description, hash, dim, uri, artworkUri, authorName, blurhash, contentWarning, mimeType)
@Immutable
abstract class MediaPreloadedContent(