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:
+44
-119
@@ -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 ->
|
||||
|
||||
+10
-14
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+40
-27
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+24
-13
@@ -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
|
||||
|
||||
+27
-15
@@ -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
|
||||
|
||||
+2
-6
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user