fix(feeds): clip + preload content warning on cropped grid cells

- Pass contentScale through to ContentWarningOverBlurhash so grid cells
  (ContentScale.Crop) use fillMaxSize instead of forcing the image's
  native aspect ratio, keeping the blurhash and overlay within the cell.
- Clip the overlay to its bounds to stop the icon/button from drawing
  over neighboring cells or other components.
- Preload the image through Coil while the warning is visible so the
  image is cached by the time the user taps "Show anyway".
This commit is contained in:
Claude
2026-04-18 22:36:04 +00:00
parent 1166654149
commit 00ab199e32
2 changed files with 28 additions and 6 deletions
@@ -40,20 +40,25 @@ 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.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.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
@@ -112,6 +117,8 @@ fun SensitivityWarningOverBlurhash(
blurhash: String?, blurhash: String?,
ratio: Float?, ratio: Float?,
description: String?, description: String?,
contentScale: ContentScale,
preloadUrl: String?,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
content: @Composable () -> Unit, content: @Composable () -> Unit,
) { ) {
@@ -124,10 +131,19 @@ fun SensitivityWarningOverBlurhash(
var showContentWarningNote by remember(accountState) { mutableStateOf(accountState.value != true) } var showContentWarningNote by remember(accountState) { mutableStateOf(accountState.value != true) }
if (showContentWarningNote && preloadUrl != null) {
val context = LocalContext.current
LaunchedEffect(preloadUrl) {
runCatching {
context.imageLoader.enqueue(ImageRequest.Builder(context).data(preloadUrl).build())
}
}
}
CrossfadeIfEnabled(targetState = showContentWarningNote, accountViewModel = accountViewModel) { CrossfadeIfEnabled(targetState = showContentWarningNote, accountViewModel = accountViewModel) {
if (it) { if (it) {
if (blurhash != null) { if (blurhash != null) {
ContentWarningOverBlurhash(reason, blurhash, ratio, description) { showContentWarningNote = false } ContentWarningOverBlurhash(reason, blurhash, ratio, description, contentScale) { showContentWarningNote = false }
} else { } else {
ContentWarningNote(reason) { showContentWarningNote = false } ContentWarningNote(reason) { showContentWarningNote = false }
} }
@@ -189,6 +205,7 @@ fun ContentWarningOverBlurhashPreview() {
blurhash = "LEHV6nWB2yk8pyo0adR*.7kCMdnj", blurhash = "LEHV6nWB2yk8pyo0adR*.7kCMdnj",
ratio = 16f / 9f, ratio = 16f / 9f,
description = null, description = null,
contentScale = ContentScale.FillWidth,
onDismiss = {}, onDismiss = {},
) )
} }
@@ -200,16 +217,17 @@ fun ContentWarningOverBlurhash(
blurhash: String, blurhash: String,
ratio: Float?, ratio: Float?,
description: String?, description: String?,
contentScale: ContentScale,
onDismiss: () -> Unit, onDismiss: () -> Unit,
) { ) {
val sizingModifier = val sizingModifier =
if (ratio != null) { when {
Modifier.fillMaxWidth().aspectRatio(ratio) contentScale == ContentScale.Crop -> Modifier.fillMaxSize()
} else { ratio != null -> Modifier.fillMaxWidth().aspectRatio(ratio)
Modifier.fillMaxWidth() else -> Modifier.fillMaxWidth()
} }
Box(modifier = sizingModifier) { Box(modifier = sizingModifier.clipToBounds()) {
DisplayBlurHash( DisplayBlurHash(
blurhash = blurhash, blurhash = blurhash,
description = description, description = description,
@@ -157,6 +157,8 @@ fun ZoomableContentView(
blurhash = content.blurhash, blurhash = content.blurhash,
ratio = ratio, ratio = ratio,
description = content.description, description = content.description,
contentScale = contentScale,
preloadUrl = content.url,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
) { ) {
TwoSecondController(content) { controllerVisible -> TwoSecondController(content) { controllerVisible ->
@@ -179,6 +181,8 @@ fun ZoomableContentView(
blurhash = content.blurhash, blurhash = content.blurhash,
ratio = ratio, ratio = ratio,
description = content.description, description = content.description,
contentScale = contentScale,
preloadUrl = null,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
) { ) {
Box( Box(