Support for NIP-36, declaration of sensitive content.

This commit is contained in:
Vitor Pamplona
2023-05-26 12:20:17 -04:00
parent d26b83a851
commit 39818268ab
20 changed files with 434 additions and 92 deletions
@@ -58,6 +58,7 @@ private object PrefKeys {
const val HIDE_BLOCK_ALERT_DIALOG = "hide_block_alert_dialog"
const val USE_PROXY = "use_proxy"
const val PROXY_PORT = "proxy_port"
const val SHOW_SENSITIVE_CONTENT = "show_sensitive_content"
val LAST_READ: (String) -> String = { route -> "last_read_route_$route" }
}
@@ -214,6 +215,12 @@ object LocalPreferences {
putBoolean(PrefKeys.HIDE_BLOCK_ALERT_DIALOG, account.hideBlockAlertDialog)
putBoolean(PrefKeys.USE_PROXY, account.proxy != null)
putInt(PrefKeys.PROXY_PORT, account.proxyPort)
if (account.showSensitiveContent == null) {
remove(PrefKeys.SHOW_SENSITIVE_CONTENT)
} else {
putBoolean(PrefKeys.SHOW_SENSITIVE_CONTENT, account.showSensitiveContent!!)
}
}.apply()
}
@@ -292,6 +299,12 @@ object LocalPreferences {
val proxyPort = getInt(PrefKeys.PROXY_PORT, 9050)
val proxy = HttpClient.initProxy(useProxy, "127.0.0.1", proxyPort)
val showSensitiveContent = if (contains(PrefKeys.SHOW_SENSITIVE_CONTENT)) {
getBoolean(PrefKeys.SHOW_SENSITIVE_CONTENT, false)
} else {
null
}
val a = Account(
Persona(privKey = privKey?.hexToByteArray(), pubKey = pubKey.hexToByteArray()),
followingChannels,
@@ -311,7 +324,8 @@ object LocalPreferences {
hideBlockAlertDialog,
latestContactList,
proxy,
proxyPort
proxyPort,
showSensitiveContent
)
return a
@@ -65,7 +65,8 @@ class Account(
var hideBlockAlertDialog: Boolean = false,
var backupContactList: ContactListEvent? = null,
var proxy: Proxy?,
var proxyPort: Int
var proxyPort: Int,
var showSensitiveContent: Boolean? = null
) {
var transientHiddenUsers: Set<String> = setOf()
@@ -480,7 +481,14 @@ class Account(
return LocalCache.notes[signedEvent.id]
}
fun sendPost(message: String, replyTo: List<Note>?, mentions: List<User>?, tags: List<String>? = null, zapReceiver: String? = null) {
fun sendPost(
message: String,
replyTo: List<Note>?,
mentions: List<User>?,
tags: List<String>? = null,
zapReceiver: String? = null,
wantsToMarkAsSensitive: Boolean
) {
if (!isWriteable()) return
val repliesToHex = replyTo?.filter { it.address() == null }?.map { it.idHex }
@@ -494,6 +502,7 @@ class Account(
addresses = addresses,
extraTags = tags,
zapReceiver = zapReceiver,
markAsSensitive = wantsToMarkAsSensitive,
privateKey = loggedIn.privKey!!
)
@@ -510,7 +519,8 @@ class Account(
valueMinimum: Int?,
consensusThreshold: Int?,
closedAt: Int?,
zapReceiver: String? = null
zapReceiver: String? = null,
wantsToMarkAsSensitive: Boolean
) {
if (!isWriteable()) return
@@ -529,14 +539,15 @@ class Account(
valueMinimum = valueMinimum,
consensusThreshold = consensusThreshold,
closedAt = closedAt,
zapReceiver = zapReceiver
zapReceiver = zapReceiver,
markAsSensitive = wantsToMarkAsSensitive
)
// println("Sending new PollNoteEvent: %s".format(signedEvent.toJson()))
Client.send(signedEvent)
LocalCache.consume(signedEvent)
}
fun sendChannelMessage(message: String, toChannel: String, replyTo: List<Note>?, mentions: List<User>?, zapReceiver: String? = null) {
fun sendChannelMessage(message: String, toChannel: String, replyTo: List<Note>?, mentions: List<User>?, zapReceiver: String? = null, wantsToMarkAsSensitive: Boolean) {
if (!isWriteable()) return
// val repliesToHex = listOfNotNull(replyingTo?.idHex).ifEmpty { null }
@@ -549,13 +560,14 @@ class Account(
replyTos = repliesToHex,
mentions = mentionsHex,
zapReceiver = zapReceiver,
markAsSensitive = wantsToMarkAsSensitive,
privateKey = loggedIn.privKey!!
)
Client.send(signedEvent)
LocalCache.consume(signedEvent, null)
}
fun sendPrivateMessage(message: String, toUser: String, replyingTo: Note? = null, mentions: List<User>?, zapReceiver: String? = null) {
fun sendPrivateMessage(message: String, toUser: String, replyingTo: Note? = null, mentions: List<User>?, zapReceiver: String? = null, wantsToMarkAsSensitive: Boolean) {
if (!isWriteable()) return
val user = LocalCache.users[toUser] ?: return
@@ -569,6 +581,7 @@ class Account(
replyTos = repliesToHex,
mentions = mentionsHex,
zapReceiver = zapReceiver,
markAsSensitive = wantsToMarkAsSensitive,
privateKey = loggedIn.privKey!!,
advertiseNip18 = false
)
@@ -1114,6 +1127,12 @@ class Account(
saveable.invalidateData()
}
fun updateShowSensitiveContent(show: Boolean?) {
showSensitiveContent = show
saveable.invalidateData()
live.invalidateData()
}
fun registerObservers() {
// Observes relays to restart connections
userProfile().live().relays.observeForever {
@@ -22,7 +22,16 @@ class ChannelMessageEvent(
companion object {
const val kind = 42
fun create(message: String, channel: String, replyTos: List<String>? = null, mentions: List<String>? = null, zapReceiver: String?, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ChannelMessageEvent {
fun create(
message: String,
channel: String,
replyTos: List<String>? = null,
mentions: List<String>? = null,
zapReceiver: String?,
privateKey: ByteArray,
createdAt: Long = Date().time / 1000,
markAsSensitive: Boolean
): ChannelMessageEvent {
val content = message
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val tags = mutableListOf(
@@ -37,6 +46,9 @@ class ChannelMessageEvent(
zapReceiver?.let {
tags.add(listOf("zap", it))
}
if (markAsSensitive) {
tags.add(listOf("content-warning", ""))
}
val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = Utils.sign(id, privateKey)
@@ -48,6 +48,12 @@ open class Event(
fun taggedUrls() = tags.filter { it.size > 1 && it[0] == "r" }.map { it[1] }
override fun isSensitive() = tags.any {
(it.size > 0 && it[0].equals("content-warning", true)) ||
(it.size > 1 && it[0] == "t" && it[1].equals("nsfw", true)) ||
(it.size > 1 && it[0] == "t" && it[1].equals("nude", true))
}
override fun zapAddress() = tags.firstOrNull { it.size > 1 && it[0] == "zap" }?.get(1)
fun taggedAddresses() = tags.filter { it.size > 1 && it[0] == "a" }.mapNotNull {
@@ -37,4 +37,5 @@ interface EventInterface {
fun getPoWRank(): Int
fun zapAddress(): String?
fun isSensitive(): Boolean
}
@@ -51,7 +51,8 @@ class PollNoteEvent(
valueMinimum: Int?,
consensusThreshold: Int?,
closedAt: Int?,
zapReceiver: String?
zapReceiver: String?,
markAsSensitive: Boolean
): PollNoteEvent {
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val tags = mutableListOf<List<String>>()
@@ -75,6 +76,9 @@ class PollNoteEvent(
if (zapReceiver != null) {
tags.add(listOf("zap", zapReceiver))
}
if (markAsSensitive) {
tags.add(listOf("content-warning", ""))
}
val id = generateId(pubKey, createdAt, kind, tags, msg)
val sig = Utils.sign(id, privateKey)
@@ -73,7 +73,8 @@ class PrivateDmEvent(
privateKey: ByteArray,
createdAt: Long = Date().time / 1000,
publishedRecipientPubKey: ByteArray? = null,
advertiseNip18: Boolean = true
advertiseNip18: Boolean = true,
markAsSensitive: Boolean
): PrivateDmEvent {
val content = Utils.encrypt(
if (advertiseNip18) { nip18Advertisement } else { "" } + msg,
@@ -94,6 +95,9 @@ class PrivateDmEvent(
zapReceiver?.let {
tags.add(listOf("zap", it))
}
if (markAsSensitive) {
tags.add(listOf("content-warning", ""))
}
val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = Utils.sign(id, privateKey)
return PrivateDmEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
@@ -29,6 +29,7 @@ class TextNoteEvent(
addresses: List<ATag>?,
extraTags: List<String>?,
zapReceiver: String?,
markAsSensitive: Boolean,
privateKey: ByteArray,
createdAt: Long = Date().time / 1000
): TextNoteEvent {
@@ -56,6 +57,9 @@ class TextNoteEvent(
findURLs(msg).forEach {
tags.add(listOf("r", it))
}
if (markAsSensitive) {
tags.add(listOf("content-warning", ""))
}
val id = generateId(pubKey, createdAt, kind, tags, msg)
val sig = Utils.sign(id, privateKey)
@@ -22,8 +22,12 @@ import androidx.compose.material.icons.filled.ArrowForwardIos
import androidx.compose.material.icons.filled.Bolt
import androidx.compose.material.icons.filled.Cancel
import androidx.compose.material.icons.filled.CurrencyBitcoin
import androidx.compose.material.icons.filled.Visibility
import androidx.compose.material.icons.filled.VisibilityOff
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material.icons.outlined.ArrowForwardIos
import androidx.compose.material.icons.outlined.Bolt
import androidx.compose.material.icons.rounded.Warning
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
@@ -83,7 +87,7 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n
val focusRequester = remember { FocusRequester() }
val keyboardController = LocalSoftwareKeyboardController.current
val scroolState = rememberScrollState()
val scrollState = rememberScrollState()
val scope = rememberCoroutineScope()
LaunchedEffect(Unit) {
@@ -153,7 +157,7 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n
Column(
modifier = Modifier
.fillMaxWidth()
.verticalScroll(scroolState)
.verticalScroll(scrollState)
) {
Notifying(postViewModel.mentions) {
postViewModel.removeFromReplyList(it)
@@ -335,10 +339,11 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n
}
if (postViewModel.canUsePoll) {
val hashtag = stringResource(R.string.poll_hashtag)
// These should be hashtag recommendations the user selects in the future.
// val hashtag = stringResource(R.string.poll_hashtag)
// postViewModel.includePollHashtagInMessage(postViewModel.wantsPoll, hashtag)
AddPollButton(postViewModel.wantsPoll) {
postViewModel.wantsPoll = !postViewModel.wantsPoll
postViewModel.includePollHashtagInMessage(postViewModel.wantsPoll, hashtag)
}
}
@@ -348,6 +353,10 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n
}
}
MarkAsSensitive(postViewModel) {
postViewModel.wantsToMarkAsSensitive = !postViewModel.wantsToMarkAsSensitive
}
ForwardZapTo(postViewModel) {
postViewModel.wantsForwardZapTo = !postViewModel.wantsForwardZapTo
}
@@ -537,6 +546,60 @@ private fun ForwardZapTo(
}
}
@Composable
private fun MarkAsSensitive(
postViewModel: NewPostViewModel,
onClick: () -> Unit
) {
IconButton(
onClick = {
onClick()
}
) {
Box(
Modifier
.height(20.dp)
.width(23.dp)
) {
if (!postViewModel.wantsToMarkAsSensitive) {
Icon(
imageVector = Icons.Default.Visibility,
contentDescription = stringResource(R.string.content_warning),
modifier = Modifier
.size(18.dp)
.align(Alignment.BottomStart),
tint = MaterialTheme.colors.onBackground
)
Icon(
imageVector = Icons.Rounded.Warning,
contentDescription = stringResource(R.string.content_warning),
modifier = Modifier
.size(10.dp)
.align(Alignment.TopEnd),
tint = MaterialTheme.colors.onBackground
)
} else {
Icon(
imageVector = Icons.Default.VisibilityOff,
contentDescription = stringResource(id = R.string.content_warning),
modifier = Modifier
.size(18.dp)
.align(Alignment.BottomStart),
tint = Color.Red
)
Icon(
imageVector = Icons.Rounded.Warning,
contentDescription = stringResource(id = R.string.content_warning),
modifier = Modifier
.size(10.dp)
.align(Alignment.TopEnd),
tint = Color.Yellow
)
}
}
}
}
@Composable
fun CloseButton(onCancel: () -> Unit) {
Button(
@@ -67,6 +67,9 @@ open class NewPostViewModel : ViewModel() {
var forwardZapTo by mutableStateOf<User?>(null)
var forwardZapToEditting by mutableStateOf(TextFieldValue(""))
// NSFW, Sensitive
var wantsToMarkAsSensitive by mutableStateOf(false)
open fun load(account: Account, replyingTo: Note?, quote: Note?) {
originalNote = replyingTo
replyingTo?.let { replyNote ->
@@ -97,6 +100,7 @@ open class NewPostViewModel : ViewModel() {
contentToAddUrl = null
wantsForwardZapTo = false
wantsToMarkAsSensitive = false
forwardZapTo = null
forwardZapToEditting = TextFieldValue("")
@@ -118,13 +122,13 @@ open class NewPostViewModel : ViewModel() {
}
if (wantsPoll) {
account?.sendPoll(tagger.message, tagger.replyTos, tagger.mentions, pollOptions, valueMaximum, valueMinimum, consensusThreshold, closedAt, zapReceiver)
account?.sendPoll(tagger.message, tagger.replyTos, tagger.mentions, pollOptions, valueMaximum, valueMinimum, consensusThreshold, closedAt, zapReceiver, wantsToMarkAsSensitive)
} else if (originalNote?.channel() != null) {
account?.sendChannelMessage(tagger.message, tagger.channel!!.idHex, tagger.replyTos, tagger.mentions, zapReceiver)
account?.sendChannelMessage(tagger.message, tagger.channel!!.idHex, tagger.replyTos, tagger.mentions, zapReceiver, wantsToMarkAsSensitive)
} else if (originalNote?.event is PrivateDmEvent) {
account?.sendPrivateMessage(tagger.message, originalNote!!.author!!.pubkeyHex, originalNote!!, tagger.mentions, zapReceiver)
account?.sendPrivateMessage(tagger.message, originalNote!!.author!!.pubkeyHex, originalNote!!, tagger.mentions, zapReceiver, wantsToMarkAsSensitive)
} else {
account?.sendPost(tagger.message, tagger.replyTos, tagger.mentions, null, zapReceiver)
account?.sendPost(tagger.message, tagger.replyTos, tagger.mentions, null, zapReceiver, wantsToMarkAsSensitive)
}
cancel()
@@ -183,6 +187,7 @@ open class NewPostViewModel : ViewModel() {
wantsInvoice = false
wantsForwardZapTo = false
wantsToMarkAsSensitive = false
forwardZapTo = null
forwardZapToEditting = TextFieldValue("")
@@ -0,0 +1,127 @@
package com.vitorpamplona.amethyst.ui.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Visibility
import androidx.compose.material.icons.rounded.Warning
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
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.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@Composable
fun SensitivityWarning(
hasSensitiveContent: Boolean,
accountViewModel: AccountViewModel,
content: @Composable () -> Unit
) {
val accountState by accountViewModel.accountLiveData.observeAsState()
var showContentWarningNote by remember(accountState) {
mutableStateOf(accountState?.account?.showSensitiveContent != true && hasSensitiveContent)
}
if (showContentWarningNote) {
ContentWarningNote() {
showContentWarningNote = false
}
} else {
content()
}
}
@Composable
fun ContentWarningNote(onDismiss: () -> Unit) {
Column() {
Row(modifier = Modifier.padding(horizontal = 12.dp)) {
Column(modifier = Modifier.padding(start = 10.dp)) {
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) {
Box(
Modifier
.height(80.dp)
.width(90.dp)
) {
Icon(
imageVector = Icons.Default.Visibility,
contentDescription = stringResource(R.string.content_warning),
modifier = Modifier
.size(70.dp)
.align(Alignment.BottomStart),
tint = MaterialTheme.colors.onBackground
)
Icon(
imageVector = Icons.Rounded.Warning,
contentDescription = stringResource(R.string.content_warning),
modifier = Modifier
.size(30.dp)
.align(Alignment.TopEnd),
tint = MaterialTheme.colors.onBackground
)
}
}
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) {
Text(
text = stringResource(R.string.content_warning),
fontWeight = FontWeight.Bold,
fontSize = 18.sp
)
}
Row() {
Text(
text = stringResource(R.string.content_warning_explanation),
color = Color.Gray,
modifier = Modifier.padding(top = 10.dp),
textAlign = TextAlign.Center
)
}
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) {
Button(
modifier = Modifier.padding(top = 10.dp),
onClick = onDismiss,
shape = RoundedCornerShape(20.dp),
colors = ButtonDefaults
.buttonColors(
backgroundColor = MaterialTheme.colors.primary
),
contentPadding = PaddingValues(vertical = 6.dp, horizontal = 16.dp)
) {
Text(
text = stringResource(R.string.show_anyway),
color = Color.White
)
}
}
}
}
}
}
@@ -55,10 +55,11 @@ class AddBountyAmountViewModel : ViewModel() {
if (newValue != null) {
account?.sendPost(
newValue.toString(),
listOfNotNull(bounty),
listOfNotNull(bounty?.author),
listOf("bounty-added-reward")
message = newValue.toString(),
replyTo = listOfNotNull(bounty),
mentions = listOfNotNull(bounty?.author),
tags = listOf("bounty-added-reward"),
wantsToMarkAsSensitive = false
)
nextAmount = TextFieldValue("")
@@ -63,6 +63,7 @@ import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
import com.vitorpamplona.amethyst.ui.components.ResizeImage
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
import com.vitorpamplona.amethyst.ui.components.SensitivityWarning
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import kotlinx.coroutines.Dispatchers
@@ -369,15 +370,21 @@ private fun RenderRegularTextNote(
val modifier = remember { Modifier.padding(top = 5.dp) }
if (eventContent != null) {
TranslatableRichTextViewer(
content = eventContent,
canPreview = canPreview,
modifier = modifier,
tags = tags,
backgroundColor = backgroundBubbleColor,
accountViewModel = accountViewModel,
nav = nav
)
val hasSensitiveContent = remember(note.event) { note.event?.isSensitive() ?: false }
SensitivityWarning(
hasSensitiveContent = hasSensitiveContent,
accountViewModel = accountViewModel
) {
TranslatableRichTextViewer(
content = eventContent,
canPreview = canPreview,
modifier = modifier,
tags = tags,
backgroundColor = backgroundBubbleColor,
accountViewModel = accountViewModel,
nav = nav
)
}
} else {
TranslatableRichTextViewer(
content = stringResource(id = R.string.could_not_decrypt_the_message),
@@ -116,6 +116,7 @@ import com.vitorpamplona.amethyst.ui.components.ResizeImage
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImage
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
import com.vitorpamplona.amethyst.ui.components.SensitivityWarning
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
import com.vitorpamplona.amethyst.ui.components.VideoView
import com.vitorpamplona.amethyst.ui.components.ZoomableContent
@@ -166,6 +167,7 @@ fun NoteCompose(
val noteEvent = remember(noteState) { note.event }
val baseChannel = remember(noteState) { note.channel() }
val isSensitive = remember(noteState) { note.event?.isSensitive() ?: false }
var popupExpanded by remember { mutableStateOf(false) }
@@ -183,7 +185,7 @@ fun NoteCompose(
note.let {
NoteQuickActionMenu(it, popupExpanded, { popupExpanded = false }, accountViewModel)
}
} else if (account.isHidden(noteForReports.author!!)) {
} else if (account.isHidden(noteForReports.author!!) || (isSensitive && account.showSensitiveContent == false)) {
// Does nothing
} else {
var showHiddenNote by remember { mutableStateOf(false) }
@@ -435,13 +437,10 @@ private fun RenderTextEvent(
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
val tags = remember(note.event?.id()) { note.event?.tags() }
val hashtags = remember(note.event?.id()) { note.event?.hashtags() ?: emptyList() }
val eventContent = remember(note.event?.id()) { accountViewModel.decrypt(note) }
val modifier = remember(note.event?.id()) { Modifier.fillMaxWidth() }
val isAuthorTheLoggedUser = remember(note.event?.id()) { accountViewModel.isLoggedUser(note.author) }
val eventContent = remember(note.event) { accountViewModel.decrypt(note) }
if (eventContent != null) {
val isAuthorTheLoggedUser = remember(note.event) { accountViewModel.isLoggedUser(note.author) }
if (makeItShort && isAuthorTheLoggedUser) {
Text(
text = eventContent,
@@ -450,16 +449,27 @@ private fun RenderTextEvent(
overflow = TextOverflow.Ellipsis
)
} else {
TranslatableRichTextViewer(
content = eventContent,
canPreview = canPreview && !makeItShort,
modifier = modifier,
tags = tags,
backgroundColor = backgroundColor,
accountViewModel = accountViewModel,
nav = nav
)
val hasSensitiveContent = remember(note.event) { note.event?.isSensitive() ?: false }
SensitivityWarning(
hasSensitiveContent = hasSensitiveContent,
accountViewModel = accountViewModel
) {
val modifier = remember(note.event) { Modifier.fillMaxWidth() }
val tags = remember(note.event) { note.event?.tags() }
TranslatableRichTextViewer(
content = eventContent,
canPreview = canPreview && !makeItShort,
modifier = modifier,
tags = tags,
backgroundColor = backgroundColor,
accountViewModel = accountViewModel,
nav = nav
)
}
val hashtags = remember(note.event) { note.event?.hashtags() ?: emptyList() }
DisplayUncitedHashtags(hashtags, eventContent, nav)
}
}
@@ -494,25 +504,31 @@ private fun RenderPoll(
overflow = TextOverflow.Ellipsis
)
} else {
TranslatableRichTextViewer(
eventContent,
canPreview = canPreview && !makeItShort,
Modifier.fillMaxWidth(),
noteEvent.tags(),
backgroundColor,
accountViewModel,
nav
)
val hasSensitiveContent = remember(note.event) { note.event?.isSensitive() ?: false }
SensitivityWarning(
hasSensitiveContent = hasSensitiveContent,
accountViewModel = accountViewModel
) {
TranslatableRichTextViewer(
eventContent,
canPreview = canPreview && !makeItShort,
Modifier.fillMaxWidth(),
noteEvent.tags(),
backgroundColor,
accountViewModel,
nav
)
PollNote(
note,
canPreview = canPreview && !makeItShort,
backgroundColor,
accountViewModel,
nav
)
}
DisplayUncitedHashtags(noteEvent.hashtags(), eventContent, nav)
PollNote(
note,
canPreview = canPreview && !makeItShort,
backgroundColor,
accountViewModel,
nav
)
}
if (!makeItShort) {
@@ -586,15 +602,21 @@ private fun RenderPrivateMessage(
overflow = TextOverflow.Ellipsis
)
} else {
TranslatableRichTextViewer(
content = eventContent,
canPreview = canPreview && !makeItShort,
modifier = modifier,
tags = tags,
backgroundColor = backgroundColor,
accountViewModel = accountViewModel,
nav = nav
)
val hasSensitiveContent = remember(note.event) { note.event?.isSensitive() ?: false }
SensitivityWarning(
hasSensitiveContent = hasSensitiveContent,
accountViewModel = accountViewModel
) {
TranslatableRichTextViewer(
content = eventContent,
canPreview = canPreview && !makeItShort,
modifier = modifier,
tags = tags,
backgroundColor = backgroundColor,
accountViewModel = accountViewModel,
nav = nav
)
}
DisplayUncitedHashtags(hashtags, eventContent, nav)
}
@@ -2124,7 +2146,9 @@ fun UserPicture(
}
val myIconModifier = remember {
Modifier.width(size.div(3.5f)).height(size.div(3.5f))
Modifier
.width(size.div(3.5f))
.height(size.div(3.5f))
}
Box(myIconBoxModifier, contentAlignment = Alignment.Center) {
@@ -2147,7 +2171,9 @@ data class DropDownParams(
val isFollowingAuthor: Boolean,
val isPrivateBookmarkNote: Boolean,
val isPublicBookmarkNote: Boolean,
val isLoggedUser: Boolean
val isLoggedUser: Boolean,
val isSensitive: Boolean,
val showSensitiveContent: Boolean?
)
@Composable
@@ -2158,20 +2184,23 @@ fun NoteDropDownMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Unit,
var reportDialogShowing by remember { mutableStateOf(false) }
val bookmarkState by accountViewModel.userProfile().live().bookmarks.observeAsState()
val accountState by accountViewModel.accountLiveData.observeAsState()
var state by remember {
mutableStateOf<DropDownParams>(
DropDownParams(false, false, false, false)
DropDownParams(false, false, false, false, false, null)
)
}
LaunchedEffect(key1 = note, key2 = bookmarkState) {
LaunchedEffect(key1 = note, key2 = bookmarkState, key3 = accountState) {
withContext(Dispatchers.IO) {
state = DropDownParams(
accountViewModel.isFollowing(note.author),
accountViewModel.isInPrivateBookmarks(note),
accountViewModel.isInPublicBookmarks(note),
accountViewModel.isLoggedUser(note.author)
isFollowingAuthor = accountViewModel.isFollowing(note.author),
isPrivateBookmarkNote = accountViewModel.isInPrivateBookmarks(note),
isPublicBookmarkNote = accountViewModel.isInPublicBookmarks(note),
isLoggedUser = accountViewModel.isLoggedUser(note.author),
isSensitive = note.event?.isSensitive() ?: false,
showSensitiveContent = accountState?.account?.showSensitiveContent
)
}
}
@@ -2249,6 +2278,24 @@ fun NoteDropDownMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Unit,
Text("Block / Report")
}
}
// if (state.isSensitive) {
Divider()
if (state.showSensitiveContent == null || state.showSensitiveContent == true) {
DropdownMenuItem(onClick = { accountViewModel.hideSensitiveContent(); onDismiss() }) {
Text(stringResource(R.string.content_warning_hide_all_sensitive_content))
}
}
if (state.showSensitiveContent == null || state.showSensitiveContent == false) {
DropdownMenuItem(onClick = { accountViewModel.disableContentWarnings(); onDismiss() }) {
Text(stringResource(R.string.content_warning_show_all_sensitive_content))
}
}
if (state.showSensitiveContent != null) {
DropdownMenuItem(onClick = { accountViewModel.seeContentWarnings(); onDismiss() }) {
Text(stringResource(R.string.content_warning_see_warnings))
}
}
// }
}
if (reportDialogShowing) {
@@ -51,7 +51,9 @@ fun PollNote(
val accountState by accountViewModel.accountLiveData.observeAsState()
val account = remember(accountState) { accountState?.account } ?: return
val pollViewModel: PollNoteViewModel = viewModel()
val pollViewModel: PollNoteViewModel = viewModel(
key = baseNote.idHex
)
LaunchedEffect(key1 = baseNote) {
pollViewModel.load(account, baseNote)
@@ -60,6 +60,7 @@ import com.vitorpamplona.amethyst.service.model.PeopleListEvent
import com.vitorpamplona.amethyst.service.model.PinListEvent
import com.vitorpamplona.amethyst.service.model.PollNoteEvent
import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status
import com.vitorpamplona.amethyst.ui.components.SensitivityWarning
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
import com.vitorpamplona.amethyst.ui.note.*
import com.vitorpamplona.amethyst.ui.note.BadgeDisplay
@@ -376,15 +377,22 @@ fun NoteMaster(
!noteForReports.hasAnyReports()
if (eventContent != null) {
TranslatableRichTextViewer(
eventContent,
canPreview,
Modifier.fillMaxWidth(),
note.event?.tags(),
MaterialTheme.colors.background,
accountViewModel,
nav
)
val hasSensitiveContent = remember(note.event) { note.event?.isSensitive() ?: false }
SensitivityWarning(
hasSensitiveContent = hasSensitiveContent,
accountViewModel = accountViewModel
) {
TranslatableRichTextViewer(
eventContent,
canPreview,
Modifier.fillMaxWidth(),
note.event?.tags(),
MaterialTheme.colors.background,
accountViewModel,
nav
)
}
DisplayUncitedHashtags(noteEvent.hashtags(), eventContent, nav)
@@ -232,6 +232,18 @@ class AccountViewModel(private val account: Account) : ViewModel() {
account.setHideBlockAlertDialog()
}
fun hideSensitiveContent() {
account.updateShowSensitiveContent(false)
}
fun disableContentWarnings() {
account.updateShowSensitiveContent(true)
}
fun seeContentWarnings() {
account.updateShowSensitiveContent(null)
}
class Factory(val account: Account) : ViewModelProvider.Factory {
override fun <AccountViewModel : ViewModel> create(modelClass: Class<AccountViewModel>): AccountViewModel {
return AccountViewModel(account) as AccountViewModel
@@ -218,7 +218,7 @@ fun ChannelScreen(
onPost = {
val tagger = NewMessageTagger(channel, listOfNotNull(replyTo.value?.author), listOfNotNull(replyTo.value), channelScreenModel.message.text)
tagger.run()
account.sendChannelMessage(tagger.message, channel.idHex, tagger.replyTos, tagger.mentions)
account.sendChannelMessage(tagger.message, channel.idHex, tagger.replyTos, tagger.mentions, wantsToMarkAsSensitive = false)
channelScreenModel.message = TextFieldValue("")
replyTo.value = null
feedViewModel.invalidateData() // Don't wait a full second before updating
@@ -178,7 +178,7 @@ fun ChatroomScreen(userId: String?, accountViewModel: AccountViewModel, nav: (St
trailingIcon = {
PostButton(
onPost = {
account.sendPrivateMessage(chatRoomScreenModel.message.text, userId, replyTo.value, null)
account.sendPrivateMessage(chatRoomScreenModel.message.text, userId, replyTo.value, null, wantsToMarkAsSensitive = false)
chatRoomScreenModel.message = TextFieldValue("")
replyTo.value = null
feedViewModel.invalidateData() // Don't wait a full second before updating
+6
View File
@@ -404,4 +404,10 @@
<string name="channel_list_join_channel">Join</string>
<string name="today">Today</string>
<string name="content_warning">Content warning</string>
<string name="content_warning_explanation">This post contains sensitive content which some people may find offensive or disturbing</string>
<string name="content_warning_hide_all_sensitive_content">Always hide sensitive content</string>
<string name="content_warning_show_all_sensitive_content">Always show sensitive content</string>
<string name="content_warning_see_warnings">Always show content warnings</string>
</resources>