diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/note/SpamCheckedNoteRender.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/note/SpamCheckedNoteRender.kt index 6924ed6235..9d38369b36 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/note/SpamCheckedNoteRender.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/note/SpamCheckedNoteRender.kt @@ -20,6 +20,15 @@ */ package com.vitorpamplona.amethyst.desktop.ui.note +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue @@ -27,6 +36,9 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.moderation.HashtagSpamCheck import com.vitorpamplona.amethyst.commons.moderation.LocalHashtagSpamSettings @@ -34,8 +46,12 @@ import com.vitorpamplona.amethyst.commons.moderation.LocalSpamExemptKeys import com.vitorpamplona.amethyst.commons.moderation.displayedEvent import com.vitorpamplona.amethyst.commons.ui.note.CollapsedSpamNote import com.vitorpamplona.amethyst.desktop.cache.DesktopLocalCache +import com.vitorpamplona.amethyst.desktop.model.LocalDesktopIAccount import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.tags.hashtags.countHashtags +import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarningReason +import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitiveOrNSFW +import kotlinx.coroutines.flow.MutableStateFlow /** * Wraps a note render in the hashtag-spam check. @@ -80,24 +96,73 @@ fun SpamCheckedNoteRender( ) } + // NIP-36 content warning: blur sensitive content unless the account opted + // into "always show sensitive content". + val account = LocalDesktopIAccount.current + val showSensitiveFlow = remember(account) { account?.showSensitiveContentSetting ?: MutableStateFlow(null) } + val showSensitive by showSensitiveFlow.collectAsState() + val isSensitive = remember(noteIdHex, displayedEvent) { displayedEvent?.isSensitiveOrNSFW() == true } + var revealed by rememberSaveable(noteIdHex) { mutableStateOf(false) } - if (!forceReveal && isSpam && !revealed && displayedEvent != null) { - val authorPubkey = displayedEvent.pubKey - val author = localCache?.getUserIfExists(authorPubkey) - val displayName = author?.toBestDisplayName() ?: authorPubkey.take(8) - val avatarUrl = author?.profilePicture() - val hashtagCount = displayedEvent.tags.countHashtags() - CollapsedSpamNote( - authorPubkeyHex = authorPubkey, - authorDisplayName = displayName, - authorAvatarUrl = avatarUrl, - hashtagCount = hashtagCount, - threshold = threshold, - onReveal = { revealed = true }, - ) - } else { - normal() + when { + forceReveal || revealed -> normal() + isSpam && displayedEvent != null -> { + val authorPubkey = displayedEvent.pubKey + val author = localCache?.getUserIfExists(authorPubkey) + val displayName = author?.toBestDisplayName() ?: authorPubkey.take(8) + val avatarUrl = author?.profilePicture() + val hashtagCount = displayedEvent.tags.countHashtags() + CollapsedSpamNote( + authorPubkeyHex = authorPubkey, + authorDisplayName = displayName, + authorAvatarUrl = avatarUrl, + hashtagCount = hashtagCount, + threshold = threshold, + onReveal = { revealed = true }, + ) + } + isSensitive && showSensitive != true && displayedEvent != null -> { + CollapsedContentWarning( + reason = displayedEvent.contentWarningReason(), + onReveal = { revealed = true }, + ) + } + else -> normal() + } +} + +/** + * Collapsed placeholder for a NIP-36 content-warning note. Mirrors the + * spam-collapse reveal affordance: the body stays hidden behind a scrim with the + * warning reason until the user taps "Show". + */ +@Composable +private fun CollapsedContentWarning( + reason: String?, + onReveal: () -> Unit, +) { + Surface( + shape = RoundedCornerShape(8.dp), + color = MaterialTheme.colorScheme.surfaceVariant, + modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth().padding(12.dp), + ) { + Column(modifier = Modifier.weight(1f)) { + Text("Sensitive content", style = MaterialTheme.typography.titleSmall) + if (!reason.isNullOrBlank()) { + Text( + reason, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + TextButton(onClick = onReveal) { Text("Show") } + } } }