feat(feeds): overlay content warning on blurhash at media size
In the picture and video feeds, render the sensitivity warning over the media's blurhash placeholder using the media's aspect ratio, instead of a fixed-size dialog that hides the context entirely. Falls back to the existing dialog when no blurhash is available.
This commit is contained in:
+157
@@ -20,10 +20,13 @@
|
|||||||
*/
|
*/
|
||||||
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.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
|
||||||
@@ -44,6 +47,7 @@ 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.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
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
|
||||||
@@ -72,6 +76,31 @@ fun SensitivityWarning(
|
|||||||
note.event?.let { SensitivityWarning(it, accountViewModel, content) }
|
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
|
@Composable
|
||||||
fun SensitivityWarning(
|
fun SensitivityWarning(
|
||||||
event: Event,
|
event: Event,
|
||||||
@@ -120,6 +149,32 @@ 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
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
fun ContentWarningNotePreview() {
|
fun ContentWarningNotePreview() {
|
||||||
@@ -144,6 +199,108 @@ fun ContentWarningNoteWithBigReasonPreview() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun ContentWarningOverBlurhashPreview() {
|
||||||
|
ThemeComparisonColumn {
|
||||||
|
ContentWarningOverBlurhash(
|
||||||
|
reason = "Spoilers",
|
||||||
|
blurhash = "LEHV6nWB2yk8pyo0adR*.7kCMdnj",
|
||||||
|
ratio = 16f / 9f,
|
||||||
|
description = null,
|
||||||
|
onDismiss = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ContentWarningOverBlurhash(
|
||||||
|
reason: String?,
|
||||||
|
blurhash: String,
|
||||||
|
ratio: Float?,
|
||||||
|
description: String?,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
) {
|
||||||
|
val sizingModifier =
|
||||||
|
if (ratio != null) {
|
||||||
|
Modifier.fillMaxWidth().aspectRatio(ratio)
|
||||||
|
} else {
|
||||||
|
Modifier.fillMaxWidth()
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(modifier = sizingModifier) {
|
||||||
|
DisplayBlurHash(
|
||||||
|
blurhash = blurhash,
|
||||||
|
description = description,
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
)
|
||||||
|
|
||||||
|
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 =
|
||||||
|
if (reason.isNullOrBlank()) {
|
||||||
|
stringRes(R.string.content_warning)
|
||||||
|
} else {
|
||||||
|
stringRes(R.string.content_warning_with_reason, reason)
|
||||||
|
},
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
fontSize = 18.sp,
|
||||||
|
color = Color.White,
|
||||||
|
softWrap = true,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
)
|
||||||
|
|
||||||
|
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?,
|
||||||
|
|||||||
+12
-2
@@ -39,6 +39,7 @@ 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.SensitivityWarning
|
||||||
@@ -112,10 +113,19 @@ private fun PictureCardImage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (images.isNotEmpty()) {
|
if (images.isNotEmpty()) {
|
||||||
SensitivityWarning(note = note, accountViewModel = accountViewModel) {
|
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(
|
ZoomableContentView(
|
||||||
content = images.first(),
|
content = first,
|
||||||
images = images,
|
images = images,
|
||||||
roundedCorner = false,
|
roundedCorner = false,
|
||||||
contentScale = ContentScale.FillWidth,
|
contentScale = ContentScale.FillWidth,
|
||||||
|
|||||||
+10
-1
@@ -42,6 +42,7 @@ 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.SensitivityWarning
|
||||||
import com.vitorpamplona.amethyst.ui.components.ZoomableContentView
|
import com.vitorpamplona.amethyst.ui.components.ZoomableContentView
|
||||||
@@ -130,7 +131,15 @@ private fun VideoCardImage(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
SensitivityWarning(note = note, accountViewModel = accountViewModel) {
|
val ratio = imeta.dimension?.aspectRatio() ?: MediaAspectRatioCache.get(imeta.url)
|
||||||
|
|
||||||
|
SensitivityWarning(
|
||||||
|
note = note,
|
||||||
|
blurhash = imeta.blurhash,
|
||||||
|
ratio = ratio,
|
||||||
|
description = content.description,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
) {
|
||||||
ZoomableContentView(
|
ZoomableContentView(
|
||||||
content = content,
|
content = content,
|
||||||
roundedCorner = false,
|
roundedCorner = false,
|
||||||
|
|||||||
Reference in New Issue
Block a user