diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/SensitivityWarning.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/SensitivityWarning.kt index 29eb4d8a19..85e57f6d42 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/SensitivityWarning.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/SensitivityWarning.kt @@ -20,10 +20,14 @@ */ package com.vitorpamplona.amethyst.ui.components +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -32,25 +36,34 @@ import androidx.compose.foundation.layout.width import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Visibility import androidx.compose.material.icons.rounded.Warning +import androidx.compose.material3.AssistChip +import androidx.compose.material3.AssistChipDefaults import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import coil3.imageLoader +import coil3.request.ImageRequest import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @@ -60,8 +73,10 @@ import com.vitorpamplona.amethyst.ui.theme.ButtonPadding import com.vitorpamplona.amethyst.ui.theme.PaddingHorizontal12Modifier import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip36SensitiveContent.ContentWarningTag import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarningReason import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitiveOrNSFW +import com.vitorpamplona.quartz.nip92IMeta.imetas @Composable fun SensitivityWarning( @@ -101,6 +116,52 @@ fun SensitivityWarning( } } +@Composable +fun ContentWarningGate( + isSensitive: Boolean, + reasons: Set, + preloadUrls: List, + accountViewModel: AccountViewModel, + modifier: Modifier = Modifier.fillMaxWidth(), + backdrop: (@Composable () -> Unit)? = null, + content: @Composable () -> Unit, +) { + if (!isSensitive) { + content() + return + } + + val accountState = accountViewModel.showSensitiveContent().collectAsStateWithLifecycle() + + var showContentWarningNote by remember(accountState) { mutableStateOf(accountState.value != true) } + + if (showContentWarningNote && preloadUrls.isNotEmpty()) { + val context = LocalContext.current + LaunchedEffect(preloadUrls) { + preloadUrls.forEach { url -> + runCatching { + context.imageLoader.enqueue(ImageRequest.Builder(context).data(url).build()) + } + } + } + } + + CrossfadeIfEnabled(targetState = showContentWarningNote, accountViewModel = accountViewModel) { + if (it) { + if (backdrop != null) { + Box(modifier = modifier.clipToBounds()) { + backdrop() + ContentWarningOverlayBody(reasons) { showContentWarningNote = false } + } + } else { + ContentWarningNote(reasons.firstOrNull()) { showContentWarningNote = false } + } + } else { + content() + } + } +} + @Composable fun ObserveSensitivityWarning( reason: String?, @@ -144,6 +205,135 @@ fun ContentWarningNoteWithBigReasonPreview() { } } +@Composable +fun BlurhashBackdrop( + blurhash: String, + description: String?, +) { + DisplayBlurHash( + blurhash = blurhash, + description = description, + contentScale = ContentScale.Crop, + modifier = Modifier.fillMaxSize(), + ) +} + +@Composable +fun BlurhashGridBackdrop(media: List) { + 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 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, + onDismiss: () -> Unit, +) { + Box( + modifier = + Modifier + .fillMaxSize() + .background(Color.Black.copy(alpha = 0.35f)), + contentAlignment = Alignment.Center, + ) { + Column( + modifier = Modifier.padding(horizontal = 12.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Box( + Modifier + .height(80.dp) + .width(90.dp), + ) { + Icon( + imageVector = Icons.Default.Visibility, + contentDescription = stringRes(R.string.content_warning), + modifier = + Modifier + .size(70.dp) + .align(Alignment.BottomStart), + tint = Color.White, + ) + Icon( + imageVector = Icons.Rounded.Warning, + contentDescription = stringRes(R.string.content_warning), + modifier = + Modifier + .size(30.dp) + .align(Alignment.TopEnd), + tint = Color.White, + ) + } + + Text( + text = stringRes(R.string.content_warning), + fontWeight = FontWeight.Bold, + fontSize = 18.sp, + color = Color.White, + softWrap = true, + textAlign = TextAlign.Center, + ) + + if (reasons.isNotEmpty()) { + FlowRow( + modifier = Modifier.padding(top = 6.dp), + horizontalArrangement = Arrangement.spacedBy(6.dp, Alignment.CenterHorizontally), + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + reasons.forEach { reason -> + AssistChip( + onClick = {}, + enabled = false, + label = { + Text( + text = reason, + color = Color.White, + ) + }, + colors = + AssistChipDefaults.assistChipColors( + disabledContainerColor = Color.White.copy(alpha = 0.15f), + disabledLabelColor = Color.White, + ), + border = null, + ) + } + } + } + + FilledTonalButton( + modifier = Modifier.padding(top = 10.dp), + onClick = onDismiss, + shape = ButtonBorder, + contentPadding = ButtonPadding, + ) { + Text( + text = stringRes(R.string.show_anyway), + ) + } + } + } +} + @Composable fun ContentWarningNote( reason: String?, @@ -221,3 +411,15 @@ fun ContentWarningNote( } } } + +fun collectContentWarningReasons(event: Event): Set { + val reasons = linkedSetOf() + event.contentWarningReason()?.takeIf { it.isNotBlank() }?.let { reasons.add(it) } + event.imetas().forEach { iMeta -> + iMeta.properties[ContentWarningTag.TAG_NAME] + ?.firstOrNull() + ?.takeIf { it.isNotBlank() } + ?.let { reasons.add(it) } + } + return reasons +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt index 08f3ccc1c5..a90440d191 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt @@ -150,7 +150,15 @@ fun ZoomableContentView( when (content) { is MediaUrlImage -> { - SensitivityWarning(content.contentWarning, accountViewModel) { + val ratio = content.dim?.aspectRatio() ?: MediaAspectRatioCache.get(content.url) + ContentWarningGate( + isSensitive = content.contentWarning != null, + reasons = setOfNotNull(content.contentWarning), + preloadUrls = listOf(content.url), + accountViewModel = accountViewModel, + modifier = mediaSizingModifier(ratio, contentScale), + backdrop = content.blurhash?.let { blurhash -> { BlurhashBackdrop(blurhash, content.description) } }, + ) { TwoSecondController(content) { controllerVisible -> val mainImageModifier = Modifier @@ -164,7 +172,15 @@ fun ZoomableContentView( } is MediaUrlVideo -> { - SensitivityWarning(content.contentWarning, accountViewModel) { + val ratio = content.dim?.aspectRatio() ?: MediaAspectRatioCache.get(content.url) + ContentWarningGate( + isSensitive = content.contentWarning != null, + reasons = setOfNotNull(content.contentWarning), + preloadUrls = emptyList(), + accountViewModel = accountViewModel, + modifier = mediaSizingModifier(ratio, contentScale), + backdrop = content.blurhash?.let { blurhash -> { BlurhashBackdrop(blurhash, content.description) } }, + ) { Box( modifier = Modifier.fillMaxWidth().then(boundsTrackingModifier), contentAlignment = Alignment.Center, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PictureDisplay.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PictureDisplay.kt index 825a9d9f42..6730e72c9e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PictureDisplay.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PictureDisplay.kt @@ -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,14 +39,20 @@ 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.SensitivityWarning +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.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 +import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitiveOrNSFW import com.vitorpamplona.quartz.nip68Picture.PictureEvent import kotlinx.collections.immutable.toImmutableList @@ -59,7 +67,9 @@ fun PictureDisplay( nav: INav, ) { val event = (note.event as? PictureEvent) ?: return - val uri = note.toNostrUri() + val uri = remember(note) { note.toNostrUri() } + val isSensitive = remember(note) { event.isSensitiveOrNSFW() } + val reasons = remember(note) { collectContentWarningReasons(event) } val images by remember(note) { @@ -85,29 +95,46 @@ fun PictureDisplay( if (first != null) { val title = event.title() - SensitivityWarning(note = note, accountViewModel = accountViewModel) { - Column { - if (title != null) { - Text( - modifier = Modifier.padding(padding), - text = title, - style = MaterialTheme.typography.bodyLarge, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - } else { - Spacer(StdVertSpacer) - } + Column { + if (title != null) { + Text( + modifier = Modifier.padding(padding), + text = title, + style = MaterialTheme.typography.bodyLarge, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } else { + Spacer(StdVertSpacer) + } - if (images.size == 1) { + if (images.size == 1) { + val ratio = first.dim?.aspectRatio() ?: MediaAspectRatioCache.get(first.url) + ContentWarningGate( + isSensitive = isSensitive, + reasons = reasons, + preloadUrls = listOf(first.url), + accountViewModel = accountViewModel, + modifier = mediaSizingModifier(ratio, ContentScale.FillWidth), + backdrop = first.blurhash?.let { blurhash -> { BlurhashBackdrop(blurhash, first.description) } }, + ) { ZoomableContentView( - content = images.first(), + content = first, images = images, roundedCorner = roundedCorner, contentScale = ContentScale.FillWidth, accountViewModel = accountViewModel, ) - } else { + } + } 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], @@ -118,20 +145,20 @@ fun PictureDisplay( ) } } - - TranslatableRichTextViewer( - content = event.content, - canPreview = false, - quotesLeft = 0, - modifier = Modifier.padding(padding), - tags = EmptyTagList, - backgroundColor = backgroundColor, - id = note.idHex, - callbackUri = uri, - accountViewModel = accountViewModel, - nav = nav, - ) } + + TranslatableRichTextViewer( + content = event.content, + canPreview = false, + quotesLeft = 0, + modifier = Modifier.padding(padding), + tags = EmptyTagList, + backgroundColor = backgroundColor, + id = note.idHex, + callbackUri = uri, + accountViewModel = accountViewModel, + nav = nav, + ) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VideoDisplay.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VideoDisplay.kt index 581ef978bd..63cb577ebe 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VideoDisplay.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VideoDisplay.kt @@ -29,12 +29,17 @@ 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.SensitivityWarning +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.isSensitiveOrNSFW import com.vitorpamplona.quartz.nip71Video.VideoEvent @Composable @@ -48,11 +53,13 @@ fun JustVideoDisplay( val event = (videoEvent as? Event) ?: return val imeta = videoEvent.imetaTags().getOrNull(0) ?: return + val isSensitive = remember(note) { event.isSensitiveOrNSFW() } + val reasons = remember(note) { collectContentWarningReasons(event) } + val isImage = remember(note) { 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) mutableStateOf( if (isImage) { @@ -80,7 +87,16 @@ fun JustVideoDisplay( ) } - SensitivityWarning(note = note, accountViewModel = accountViewModel) { + 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, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/PictureCardCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/PictureCardCompose.kt index 864eaacc85..d7aa97489a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/PictureCardCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/PictureCardCompose.kt @@ -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,14 +40,20 @@ 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.SensitivityWarning +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.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.nip36SensitiveContent.isSensitiveOrNSFW import com.vitorpamplona.quartz.nip68Picture.PictureEvent import kotlinx.collections.immutable.toImmutableList @@ -90,7 +97,9 @@ private fun PictureCardImage( backgroundColor: MutableState, accountViewModel: AccountViewModel, ) { - val uri = note.toNostrUri() + val uri = remember(note) { note.toNostrUri() } + val isSensitive = remember(note) { event.isSensitiveOrNSFW() } + val reasons = remember(note) { collectContentWarningReasons(event) } val images by remember(note) { @@ -111,26 +120,44 @@ private fun PictureCardImage( ) } - if (images.isNotEmpty()) { - SensitivityWarning(note = note, accountViewModel = accountViewModel) { - 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 = single, + images = images, + roundedCorner = false, + 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( - content = images.first(), + content = images[it], images = images, roundedCorner = false, - contentScale = ContentScale.FillWidth, + contentScale = ContentScale.Crop, accountViewModel = accountViewModel, ) - } else { - AutoNonlazyGrid(images.size) { - ZoomableContentView( - content = images[it], - images = images, - roundedCorner = false, - contentScale = ContentScale.Crop, - accountViewModel = accountViewModel, - ) - } } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/VideoCardCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/VideoCardCompose.kt index ba77fe0b6d..11385dd60b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/VideoCardCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/VideoCardCompose.kt @@ -42,15 +42,20 @@ 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.SensitivityWarning +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.isSensitiveOrNSFW import com.vitorpamplona.quartz.nip71Video.VideoEvent import kotlin.text.ifEmpty @@ -98,11 +103,13 @@ private fun VideoCardImage( val event = (event as? Event) ?: return val imeta = videoEvent.imetaTags().getOrNull(0) ?: return + val isSensitive = remember(note) { event.isSensitiveOrNSFW() } + val reasons = remember(note) { collectContentWarningReasons(event) } + val isImage = remember(note) { 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) mutableStateOf( if (isImage) { @@ -130,7 +137,16 @@ private fun VideoCardImage( ) } - SensitivityWarning(note = note, accountViewModel = accountViewModel) { + 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, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/FileHeaderCardCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/FileHeaderCardCompose.kt index 3b6dca98a3..2efc3243a6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/FileHeaderCardCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/FileHeaderCardCompose.kt @@ -42,14 +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.SensitivityWarning +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.isSensitiveOrNSFW import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent import kotlin.text.ifEmpty @@ -95,12 +100,15 @@ private fun FileHeaderCardImage( ) { val fullUrl = event.url() ?: return + val isSensitive = remember(note) { event.isSensitiveOrNSFW() } + val reasons = remember(note) { collectContentWarningReasons(event) } + val isImage = remember(note) { event.mimeType()?.startsWith("image/") == true || RichTextParser.isImageUrl(fullUrl) } + val blurHash = remember(note) { event.blurhash() } + val dimensions = remember(note) { 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() @@ -130,7 +138,16 @@ private fun FileHeaderCardImage( ) } - SensitivityWarning(note = note, accountViewModel = accountViewModel) { + 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,