Merge pull request #2447 from vitorpamplona/claude/content-warning-media-overlay-zjdoP
Refactor content warning UI with backdrop support and image preloading
This commit is contained in:
+202
@@ -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<String>,
|
||||
preloadUrls: List<String>,
|
||||
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<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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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>,
|
||||
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<String> {
|
||||
val reasons = linkedSetOf<String>()
|
||||
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
|
||||
}
|
||||
|
||||
+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)
|
||||
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,
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<BaseMediaContent>(
|
||||
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,
|
||||
|
||||
+44
-17
@@ -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<Color>,
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+19
-3
@@ -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<BaseMediaContent>(
|
||||
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,
|
||||
|
||||
+22
-5
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user