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
|
package com.vitorpamplona.amethyst.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
import androidx.compose.foundation.layout.Row
|
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.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
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.Icons
|
||||||
import androidx.compose.material.icons.filled.Visibility
|
import androidx.compose.material.icons.filled.Visibility
|
||||||
import androidx.compose.material.icons.rounded.Warning
|
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.FilledTonalButton
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clipToBounds
|
||||||
import androidx.compose.ui.graphics.Color
|
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.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import coil3.imageLoader
|
||||||
|
import coil3.request.ImageRequest
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
|
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
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.PaddingHorizontal12Modifier
|
||||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
|
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||||
|
import com.vitorpamplona.quartz.nip36SensitiveContent.ContentWarningTag
|
||||||
import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarningReason
|
import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarningReason
|
||||||
import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitiveOrNSFW
|
import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitiveOrNSFW
|
||||||
|
import com.vitorpamplona.quartz.nip92IMeta.imetas
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SensitivityWarning(
|
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
|
@Composable
|
||||||
fun ObserveSensitivityWarning(
|
fun ObserveSensitivityWarning(
|
||||||
reason: String?,
|
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
|
@Composable
|
||||||
fun ContentWarningNote(
|
fun ContentWarningNote(
|
||||||
reason: String?,
|
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) {
|
when (content) {
|
||||||
is MediaUrlImage -> {
|
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 ->
|
TwoSecondController(content) { controllerVisible ->
|
||||||
val mainImageModifier =
|
val mainImageModifier =
|
||||||
Modifier
|
Modifier
|
||||||
@@ -164,7 +172,15 @@ fun ZoomableContentView(
|
|||||||
}
|
}
|
||||||
|
|
||||||
is MediaUrlVideo -> {
|
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(
|
Box(
|
||||||
modifier = Modifier.fillMaxWidth().then(boundsTrackingModifier),
|
modifier = Modifier.fillMaxWidth().then(boundsTrackingModifier),
|
||||||
contentAlignment = Alignment.Center,
|
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.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Spacer
|
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.foundation.layout.padding
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
@@ -37,14 +39,20 @@ import androidx.compose.ui.layout.ContentScale
|
|||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import com.vitorpamplona.amethyst.commons.model.EmptyTagList
|
import com.vitorpamplona.amethyst.commons.model.EmptyTagList
|
||||||
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage
|
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage
|
||||||
|
import com.vitorpamplona.amethyst.model.MediaAspectRatioCache
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.ui.components.AutoNonlazyGrid
|
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.TranslatableRichTextViewer
|
||||||
import com.vitorpamplona.amethyst.ui.components.ZoomableContentView
|
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.navigation.navs.INav
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||||
|
import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitiveOrNSFW
|
||||||
import com.vitorpamplona.quartz.nip68Picture.PictureEvent
|
import com.vitorpamplona.quartz.nip68Picture.PictureEvent
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
||||||
@@ -59,7 +67,9 @@ fun PictureDisplay(
|
|||||||
nav: INav,
|
nav: INav,
|
||||||
) {
|
) {
|
||||||
val event = (note.event as? PictureEvent) ?: return
|
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
|
val images by
|
||||||
remember(note) {
|
remember(note) {
|
||||||
@@ -85,29 +95,46 @@ fun PictureDisplay(
|
|||||||
if (first != null) {
|
if (first != null) {
|
||||||
val title = event.title()
|
val title = event.title()
|
||||||
|
|
||||||
SensitivityWarning(note = note, accountViewModel = accountViewModel) {
|
Column {
|
||||||
Column {
|
if (title != null) {
|
||||||
if (title != null) {
|
Text(
|
||||||
Text(
|
modifier = Modifier.padding(padding),
|
||||||
modifier = Modifier.padding(padding),
|
text = title,
|
||||||
text = title,
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
maxLines = 1,
|
||||||
maxLines = 1,
|
overflow = TextOverflow.Ellipsis,
|
||||||
overflow = TextOverflow.Ellipsis,
|
)
|
||||||
)
|
} else {
|
||||||
} else {
|
Spacer(StdVertSpacer)
|
||||||
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(
|
ZoomableContentView(
|
||||||
content = images.first(),
|
content = first,
|
||||||
images = images,
|
images = images,
|
||||||
roundedCorner = roundedCorner,
|
roundedCorner = roundedCorner,
|
||||||
contentScale = ContentScale.FillWidth,
|
contentScale = ContentScale.FillWidth,
|
||||||
accountViewModel = accountViewModel,
|
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) {
|
AutoNonlazyGrid(images.size) {
|
||||||
ZoomableContentView(
|
ZoomableContentView(
|
||||||
content = images[it],
|
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.MediaUrlImage
|
||||||
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlVideo
|
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlVideo
|
||||||
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
|
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
|
||||||
|
import com.vitorpamplona.amethyst.model.MediaAspectRatioCache
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
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.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.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||||
import com.vitorpamplona.quartz.nip31Alts.alt
|
import com.vitorpamplona.quartz.nip31Alts.alt
|
||||||
|
import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitiveOrNSFW
|
||||||
import com.vitorpamplona.quartz.nip71Video.VideoEvent
|
import com.vitorpamplona.quartz.nip71Video.VideoEvent
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -48,11 +53,13 @@ fun JustVideoDisplay(
|
|||||||
val event = (videoEvent as? Event) ?: return
|
val event = (videoEvent as? Event) ?: return
|
||||||
|
|
||||||
val imeta = videoEvent.imetaTags().getOrNull(0) ?: 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
|
val content by
|
||||||
remember(note) {
|
remember(note) {
|
||||||
val description = event.content.ifEmpty { null } ?: imeta.alt ?: event.alt()
|
val description = event.content.ifEmpty { null } ?: imeta.alt ?: event.alt()
|
||||||
val isImage = imeta.mimeType?.startsWith("image/") == true || RichTextParser.isImageUrl(imeta.url)
|
|
||||||
|
|
||||||
mutableStateOf<BaseMediaContent>(
|
mutableStateOf<BaseMediaContent>(
|
||||||
if (isImage) {
|
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(
|
ZoomableContentView(
|
||||||
content = content,
|
content = content,
|
||||||
roundedCorner = roundedCorner,
|
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.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
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.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage
|
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage
|
||||||
|
import com.vitorpamplona.amethyst.model.MediaAspectRatioCache
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.ui.components.AutoNonlazyGrid
|
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.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.navigation.navs.INav
|
||||||
import com.vitorpamplona.amethyst.ui.note.ReactionsRow
|
import com.vitorpamplona.amethyst.ui.note.ReactionsRow
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.UserCardHeader
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.UserCardHeader
|
||||||
|
import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitiveOrNSFW
|
||||||
import com.vitorpamplona.quartz.nip68Picture.PictureEvent
|
import com.vitorpamplona.quartz.nip68Picture.PictureEvent
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
||||||
@@ -90,7 +97,9 @@ private fun PictureCardImage(
|
|||||||
backgroundColor: MutableState<Color>,
|
backgroundColor: MutableState<Color>,
|
||||||
accountViewModel: AccountViewModel,
|
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
|
val images by
|
||||||
remember(note) {
|
remember(note) {
|
||||||
@@ -111,26 +120,44 @@ private fun PictureCardImage(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (images.isNotEmpty()) {
|
if (images.isEmpty()) return
|
||||||
SensitivityWarning(note = note, accountViewModel = accountViewModel) {
|
|
||||||
if (images.size == 1) {
|
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(
|
ZoomableContentView(
|
||||||
content = images.first(),
|
content = images[it],
|
||||||
images = images,
|
images = images,
|
||||||
roundedCorner = false,
|
roundedCorner = false,
|
||||||
contentScale = ContentScale.FillWidth,
|
contentScale = ContentScale.Crop,
|
||||||
accountViewModel = accountViewModel,
|
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.MediaUrlImage
|
||||||
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlVideo
|
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlVideo
|
||||||
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
|
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
|
||||||
|
import com.vitorpamplona.amethyst.model.MediaAspectRatioCache
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
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.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.navigation.navs.INav
|
||||||
import com.vitorpamplona.amethyst.ui.note.ReactionsRow
|
import com.vitorpamplona.amethyst.ui.note.ReactionsRow
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.UserCardHeader
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.UserCardHeader
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||||
import com.vitorpamplona.quartz.nip31Alts.alt
|
import com.vitorpamplona.quartz.nip31Alts.alt
|
||||||
|
import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitiveOrNSFW
|
||||||
import com.vitorpamplona.quartz.nip71Video.VideoEvent
|
import com.vitorpamplona.quartz.nip71Video.VideoEvent
|
||||||
import kotlin.text.ifEmpty
|
import kotlin.text.ifEmpty
|
||||||
|
|
||||||
@@ -98,11 +103,13 @@ private fun VideoCardImage(
|
|||||||
val event = (event as? Event) ?: return
|
val event = (event as? Event) ?: return
|
||||||
|
|
||||||
val imeta = videoEvent.imetaTags().getOrNull(0) ?: 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
|
val content by
|
||||||
remember(note) {
|
remember(note) {
|
||||||
val description = event.content.ifEmpty { null } ?: imeta.alt ?: event.alt()
|
val description = event.content.ifEmpty { null } ?: imeta.alt ?: event.alt()
|
||||||
val isImage = imeta.mimeType?.startsWith("image/") == true || RichTextParser.isImageUrl(imeta.url)
|
|
||||||
|
|
||||||
mutableStateOf<BaseMediaContent>(
|
mutableStateOf<BaseMediaContent>(
|
||||||
if (isImage) {
|
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(
|
ZoomableContentView(
|
||||||
content = content,
|
content = content,
|
||||||
roundedCorner = false,
|
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.MediaUrlImage
|
||||||
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlVideo
|
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlVideo
|
||||||
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
|
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
|
||||||
|
import com.vitorpamplona.amethyst.model.MediaAspectRatioCache
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
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.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.navigation.navs.INav
|
||||||
import com.vitorpamplona.amethyst.ui.note.ReactionsRow
|
import com.vitorpamplona.amethyst.ui.note.ReactionsRow
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||||
import com.vitorpamplona.quartz.nip31Alts.alt
|
import com.vitorpamplona.quartz.nip31Alts.alt
|
||||||
|
import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitiveOrNSFW
|
||||||
import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent
|
import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent
|
||||||
import kotlin.text.ifEmpty
|
import kotlin.text.ifEmpty
|
||||||
|
|
||||||
@@ -95,12 +100,15 @@ private fun FileHeaderCardImage(
|
|||||||
) {
|
) {
|
||||||
val fullUrl = event.url() ?: return
|
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 content by remember(note) {
|
||||||
val blurHash = event.blurhash()
|
|
||||||
val hash = event.hash()
|
val hash = event.hash()
|
||||||
val dimensions = event.dimensions()
|
|
||||||
val description = event.content.ifEmpty { null } ?: event.alt()
|
val description = event.content.ifEmpty { null } ?: event.alt()
|
||||||
val isImage = event.mimeType()?.startsWith("image/") == true || RichTextParser.isImageUrl(fullUrl)
|
|
||||||
val uri = note.toNostrUri()
|
val uri = note.toNostrUri()
|
||||||
val mimeType = event.mimeType()
|
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(
|
ZoomableContentView(
|
||||||
content = content,
|
content = content,
|
||||||
roundedCorner = false,
|
roundedCorner = false,
|
||||||
|
|||||||
Reference in New Issue
Block a user