refactor(feeds): push content warning into ZoomableContentView per media
Move the blurhash-aware sensitivity warning from feed card wrappers into
ZoomableContentView so every media item shows its own warning sized to
its own aspect ratio over its own blurhash. Grid posts now warn per
image instead of once over the whole grid.
- Add isSensitive to MediaUrl{Image,Video} (defaults to contentWarning != null)
- Populate isSensitive and contentWarning when building media in the
picture, video, and file-header feeds plus their display variants
- Drop the outer SensitivityWarning wrappers now handled inside
ZoomableContentView via SensitivityWarningOverBlurhash
This commit is contained in:
+32
-51
@@ -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() {
|
||||
|
||||
+18
-2
@@ -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,
|
||||
|
||||
@@ -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,7 +90,6 @@ fun PictureDisplay(
|
||||
if (first != null) {
|
||||
val title = event.title()
|
||||
|
||||
SensitivityWarning(note = note, accountViewModel = accountViewModel) {
|
||||
Column {
|
||||
if (title != null) {
|
||||
Text(
|
||||
@@ -134,4 +138,3 @@ fun PictureDisplay(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<BaseMediaContent>(
|
||||
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,13 +79,14 @@ 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,
|
||||
@@ -88,4 +94,3 @@ fun JustVideoDisplay(
|
||||
accountViewModel = accountViewModel,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+7
-14
@@ -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,19 +117,9 @@ 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) {
|
||||
ZoomableContentView(
|
||||
content = first,
|
||||
content = images.first(),
|
||||
images = images,
|
||||
roundedCorner = false,
|
||||
contentScale = ContentScale.FillWidth,
|
||||
@@ -144,7 +138,6 @@ private fun PictureCardImage(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun PictureCardCaption(event: PictureEvent) {
|
||||
|
||||
+8
-12
@@ -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<BaseMediaContent>(
|
||||
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,21 +129,14 @@ 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,
|
||||
accountViewModel = accountViewModel,
|
||||
) {
|
||||
ZoomableContentView(
|
||||
content = content,
|
||||
roundedCorner = false,
|
||||
@@ -147,7 +144,6 @@ private fun VideoCardImage(
|
||||
accountViewModel = accountViewModel,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun VideoCardCaption(videoEvent: VideoEvent) {
|
||||
|
||||
+8
-3
@@ -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<BaseMediaContent>(
|
||||
if (isImage) {
|
||||
@@ -113,6 +116,8 @@ private fun FileHeaderCardImage(
|
||||
blurhash = blurHash,
|
||||
dim = dimensions,
|
||||
uri = uri,
|
||||
contentWarning = contentWarning,
|
||||
isSensitive = isSensitive,
|
||||
mimeType = mimeType,
|
||||
)
|
||||
} else {
|
||||
@@ -124,13 +129,14 @@ 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,
|
||||
@@ -138,7 +144,6 @@ private fun FileHeaderCardImage(
|
||||
accountViewModel = accountViewModel,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun FileHeaderCardCaption(videoEvent: FileHeaderEvent) {
|
||||
|
||||
+6
-2
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user