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 387c6c99c..0375cfa1a 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 @@ -76,31 +76,6 @@ fun SensitivityWarning( note.event?.let { SensitivityWarning(it, accountViewModel, content) } } -@Composable -fun SensitivityWarning( - note: Note, - blurhash: String?, - ratio: Float?, - description: String?, - accountViewModel: AccountViewModel, - content: @Composable () -> Unit, -) { - val event = note.event - if (event == null) { - content() - return - } - - val hasSensitiveContent = remember(event) { event.isSensitiveOrNSFW() } - - if (hasSensitiveContent) { - val reason = remember(event) { event.contentWarningReason() } - ObserveSensitivityWarningOverBlurhash(reason, blurhash, ratio, description, accountViewModel, content) - } else { - content() - } -} - @Composable fun SensitivityWarning( event: Event, @@ -130,6 +105,38 @@ fun SensitivityWarning( } } +@Composable +fun SensitivityWarningOverBlurhash( + isSensitive: Boolean, + reason: String?, + blurhash: String?, + ratio: Float?, + description: String?, + accountViewModel: AccountViewModel, + content: @Composable () -> Unit, +) { + if (!isSensitive) { + content() + return + } + + val accountState = accountViewModel.showSensitiveContent().collectAsStateWithLifecycle() + + var showContentWarningNote by remember(accountState) { mutableStateOf(accountState.value != true) } + + CrossfadeIfEnabled(targetState = showContentWarningNote, accountViewModel = accountViewModel) { + if (it) { + if (blurhash != null) { + ContentWarningOverBlurhash(reason, blurhash, ratio, description) { showContentWarningNote = false } + } else { + ContentWarningNote(reason) { showContentWarningNote = false } + } + } else { + content() + } + } +} + @Composable fun ObserveSensitivityWarning( reason: String?, @@ -149,32 +156,6 @@ fun ObserveSensitivityWarning( } } -@Composable -fun ObserveSensitivityWarningOverBlurhash( - reason: String?, - blurhash: String?, - ratio: Float?, - description: String?, - accountViewModel: AccountViewModel, - content: @Composable () -> Unit, -) { - val accountState = accountViewModel.showSensitiveContent().collectAsStateWithLifecycle() - - var showContentWarningNote by remember(accountState) { mutableStateOf(accountState.value != true) } - - CrossfadeIfEnabled(targetState = showContentWarningNote, accountViewModel = accountViewModel) { - if (it) { - if (blurhash != null) { - ContentWarningOverBlurhash(reason, blurhash, ratio, description) { showContentWarningNote = false } - } else { - ContentWarningNote(reason) { showContentWarningNote = false } - } - } else { - content() - } - } -} - @Preview @Composable fun ContentWarningNotePreview() { 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 08f3ccc1c..1de17f9ea 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) + SensitivityWarningOverBlurhash( + isSensitive = content.isSensitive, + reason = content.contentWarning, + blurhash = content.blurhash, + ratio = ratio, + description = content.description, + accountViewModel = accountViewModel, + ) { 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) + SensitivityWarningOverBlurhash( + isSensitive = content.isSensitive, + reason = content.contentWarning, + blurhash = content.blurhash, + ratio = ratio, + description = content.description, + accountViewModel = accountViewModel, + ) { 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 825a9d9f4..f6ac70130 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 @@ -39,12 +39,13 @@ import com.vitorpamplona.amethyst.commons.model.EmptyTagList import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage 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.TranslatableRichTextViewer import com.vitorpamplona.amethyst.ui.components.ZoomableContentView 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.contentWarningReason +import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitiveOrNSFW import com.vitorpamplona.quartz.nip68Picture.PictureEvent import kotlinx.collections.immutable.toImmutableList @@ -60,6 +61,8 @@ fun PictureDisplay( ) { val event = (note.event as? PictureEvent) ?: return val uri = note.toNostrUri() + val isSensitive = event.isSensitiveOrNSFW() + val contentWarning = event.contentWarningReason() val images by remember(note) { @@ -74,6 +77,8 @@ fun PictureDisplay( blurhash = it.blurhash, dim = it.dimension, uri = uri, + contentWarning = contentWarning, + isSensitive = isSensitive, mimeType = it.mimeType, ) }.toImmutableList(), @@ -85,53 +90,51 @@ 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) { + ZoomableContentView( + content = images.first(), + images = images, + roundedCorner = roundedCorner, + contentScale = ContentScale.FillWidth, + accountViewModel = accountViewModel, + ) + } else { + AutoNonlazyGrid(images.size) { ZoomableContentView( - content = images.first(), + content = images[it], images = images, roundedCorner = roundedCorner, - contentScale = ContentScale.FillWidth, + contentScale = ContentScale.Crop, accountViewModel = accountViewModel, ) - } else { - AutoNonlazyGrid(images.size) { - ZoomableContentView( - content = images[it], - images = images, - roundedCorner = roundedCorner, - contentScale = ContentScale.Crop, - accountViewModel = accountViewModel, - ) - } } - - 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 581ef978b..a5b73df9e 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 @@ -30,11 +30,12 @@ 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.Note -import com.vitorpamplona.amethyst.ui.components.SensitivityWarning import com.vitorpamplona.amethyst.ui.components.ZoomableContentView 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 @Composable @@ -53,6 +54,8 @@ fun JustVideoDisplay( 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( if (isImage) { @@ -63,6 +66,8 @@ fun JustVideoDisplay( blurhash = imeta.blurhash, dim = imeta.dimension, uri = note.toNostrUri(), + contentWarning = contentWarning, + isSensitive = isSensitive, mimeType = imeta.mimeType, ) } else { @@ -74,18 +79,18 @@ fun JustVideoDisplay( dim = imeta.dimension, uri = note.toNostrUri(), authorName = note.author?.toBestDisplayName(), + contentWarning = contentWarning, + isSensitive = isSensitive, mimeType = imeta.mimeType, ) }, ) } - SensitivityWarning(note = note, accountViewModel = accountViewModel) { - ZoomableContentView( - content = content, - roundedCorner = roundedCorner, - contentScale = contentScale, - accountViewModel = accountViewModel, - ) - } + ZoomableContentView( + content = content, + roundedCorner = roundedCorner, + contentScale = contentScale, + accountViewModel = accountViewModel, + ) } 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 325e3313b..8cb831ca8 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 @@ -39,15 +39,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.SensitivityWarning import com.vitorpamplona.amethyst.ui.components.ZoomableContentView 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.contentWarningReason +import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitiveOrNSFW import com.vitorpamplona.quartz.nip68Picture.PictureEvent import kotlinx.collections.immutable.toImmutableList @@ -92,6 +92,8 @@ private fun PictureCardImage( accountViewModel: AccountViewModel, ) { val uri = note.toNostrUri() + val isSensitive = event.isSensitiveOrNSFW() + val contentWarning = event.contentWarningReason() val images by remember(note) { @@ -106,6 +108,8 @@ private fun PictureCardImage( blurhash = it.blurhash, dim = it.dimension, uri = uri, + contentWarning = contentWarning, + isSensitive = isSensitive, mimeType = it.mimeType, ) }.toImmutableList(), @@ -113,34 +117,23 @@ private fun PictureCardImage( } if (images.isNotEmpty()) { - val first = images.first() - val ratio = first.dim?.aspectRatio() ?: MediaAspectRatioCache.get(first.url) - - SensitivityWarning( - note = note, - blurhash = first.blurhash, - ratio = ratio, - description = first.description, - accountViewModel = accountViewModel, - ) { - if (images.size == 1) { + if (images.size == 1) { + ZoomableContentView( + content = images.first(), + images = images, + roundedCorner = false, + contentScale = ContentScale.FillWidth, + accountViewModel = accountViewModel, + ) + } else { + AutoNonlazyGrid(images.size) { ZoomableContentView( - content = 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 18dbd921d..21cab732e 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,9 +42,7 @@ 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.ZoomableContentView import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.ReactionsRow @@ -52,6 +50,8 @@ 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 @@ -104,6 +104,8 @@ private fun VideoCardImage( 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( if (isImage) { @@ -114,6 +116,8 @@ private fun VideoCardImage( blurhash = imeta.blurhash, dim = imeta.dimension, uri = note.toNostrUri(), + contentWarning = contentWarning, + isSensitive = isSensitive, mimeType = imeta.mimeType, ) } else { @@ -125,28 +129,20 @@ private fun VideoCardImage( dim = imeta.dimension, uri = note.toNostrUri(), authorName = note.author?.toBestDisplayName(), + contentWarning = contentWarning, + isSensitive = isSensitive, mimeType = imeta.mimeType, ) }, ) } - val ratio = imeta.dimension?.aspectRatio() ?: MediaAspectRatioCache.get(imeta.url) - - SensitivityWarning( - note = note, - blurhash = imeta.blurhash, - ratio = ratio, - description = content.description, + ZoomableContentView( + content = content, + roundedCorner = false, + contentScale = ContentScale.FillWidth, accountViewModel = accountViewModel, - ) { - ZoomableContentView( - content = content, - roundedCorner = false, - contentScale = ContentScale.FillWidth, - accountViewModel = accountViewModel, - ) - } + ) } @Composable 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 3b6dca98a..c117b8eba 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 @@ -43,13 +43,14 @@ 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.Note -import com.vitorpamplona.amethyst.ui.components.SensitivityWarning import com.vitorpamplona.amethyst.ui.components.ZoomableContentView 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 @@ -103,6 +104,8 @@ private fun FileHeaderCardImage( 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( if (isImage) { @@ -113,6 +116,8 @@ private fun FileHeaderCardImage( blurhash = blurHash, dim = dimensions, uri = uri, + contentWarning = contentWarning, + isSensitive = isSensitive, mimeType = mimeType, ) } else { @@ -124,20 +129,20 @@ private fun FileHeaderCardImage( dim = dimensions, uri = uri, authorName = note.author?.toBestDisplayName(), + contentWarning = contentWarning, + isSensitive = isSensitive, mimeType = mimeType, ) }, ) } - SensitivityWarning(note = note, accountViewModel = accountViewModel) { - ZoomableContentView( - content = content, - roundedCorner = false, - contentScale = ContentScale.FillWidth, - accountViewModel = accountViewModel, - ) - } + ZoomableContentView( + content = content, + roundedCorner = false, + contentScale = ContentScale.FillWidth, + accountViewModel = accountViewModel, + ) } @Composable diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/MediaContentModels.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/MediaContentModels.kt index d390a1834..5f9d30c06 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/MediaContentModels.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/MediaContentModels.kt @@ -51,6 +51,7 @@ 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) @@ -62,11 +63,12 @@ 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, mimeType) +) : MediaUrlImage(url, description, hash, blurhash, dim, uri, contentWarning, isSensitive, mimeType) @Immutable open class MediaUrlVideo( @@ -79,6 +81,7 @@ 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) @@ -93,11 +96,12 @@ 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, mimeType) +) : MediaUrlVideo(url, description, hash, dim, uri, artworkUri, authorName, blurhash, contentWarning, isSensitive, mimeType) @Immutable abstract class MediaPreloadedContent(