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 HIDE_BLOCK_ALERT_DIALOG = "hide_block_alert_dialog"
const val USE_PROXY = "use_proxy" const val USE_PROXY = "use_proxy"
const val PROXY_PORT = "proxy_port" const val PROXY_PORT = "proxy_port"
const val SHOW_SENSITIVE_CONTENT = "show_sensitive_content"
val LAST_READ: (String) -> String = { route -> "last_read_route_$route" } 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.HIDE_BLOCK_ALERT_DIALOG, account.hideBlockAlertDialog)
putBoolean(PrefKeys.USE_PROXY, account.proxy != null) putBoolean(PrefKeys.USE_PROXY, account.proxy != null)
putInt(PrefKeys.PROXY_PORT, account.proxyPort) putInt(PrefKeys.PROXY_PORT, account.proxyPort)
if (account.showSensitiveContent == null) {
remove(PrefKeys.SHOW_SENSITIVE_CONTENT)
} else {
putBoolean(PrefKeys.SHOW_SENSITIVE_CONTENT, account.showSensitiveContent!!)
}
}.apply() }.apply()
} }
@@ -292,6 +299,12 @@ object LocalPreferences {
val proxyPort = getInt(PrefKeys.PROXY_PORT, 9050) val proxyPort = getInt(PrefKeys.PROXY_PORT, 9050)
val proxy = HttpClient.initProxy(useProxy, "127.0.0.1", proxyPort) 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( val a = Account(
Persona(privKey = privKey?.hexToByteArray(), pubKey = pubKey.hexToByteArray()), Persona(privKey = privKey?.hexToByteArray(), pubKey = pubKey.hexToByteArray()),
followingChannels, followingChannels,
@@ -311,7 +324,8 @@ object LocalPreferences {
hideBlockAlertDialog, hideBlockAlertDialog,
latestContactList, latestContactList,
proxy, proxy,
proxyPort proxyPort,
showSensitiveContent
) )
return a return a
@@ -65,7 +65,8 @@ class Account(
var hideBlockAlertDialog: Boolean = false, var hideBlockAlertDialog: Boolean = false,
var backupContactList: ContactListEvent? = null, var backupContactList: ContactListEvent? = null,
var proxy: Proxy?, var proxy: Proxy?,
var proxyPort: Int var proxyPort: Int,
var showSensitiveContent: Boolean? = null
) { ) {
var transientHiddenUsers: Set<String> = setOf() var transientHiddenUsers: Set<String> = setOf()
@@ -480,7 +481,14 @@ class Account(
return LocalCache.notes[signedEvent.id] 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 if (!isWriteable()) return
val repliesToHex = replyTo?.filter { it.address() == null }?.map { it.idHex } val repliesToHex = replyTo?.filter { it.address() == null }?.map { it.idHex }
@@ -494,6 +502,7 @@ class Account(
addresses = addresses, addresses = addresses,
extraTags = tags, extraTags = tags,
zapReceiver = zapReceiver, zapReceiver = zapReceiver,
markAsSensitive = wantsToMarkAsSensitive,
privateKey = loggedIn.privKey!! privateKey = loggedIn.privKey!!
) )
@@ -510,7 +519,8 @@ class Account(
valueMinimum: Int?, valueMinimum: Int?,
consensusThreshold: Int?, consensusThreshold: Int?,
closedAt: Int?, closedAt: Int?,
zapReceiver: String? = null zapReceiver: String? = null,
wantsToMarkAsSensitive: Boolean
) { ) {
if (!isWriteable()) return if (!isWriteable()) return
@@ -529,14 +539,15 @@ class Account(
valueMinimum = valueMinimum, valueMinimum = valueMinimum,
consensusThreshold = consensusThreshold, consensusThreshold = consensusThreshold,
closedAt = closedAt, closedAt = closedAt,
zapReceiver = zapReceiver zapReceiver = zapReceiver,
markAsSensitive = wantsToMarkAsSensitive
) )
// println("Sending new PollNoteEvent: %s".format(signedEvent.toJson())) // println("Sending new PollNoteEvent: %s".format(signedEvent.toJson()))
Client.send(signedEvent) Client.send(signedEvent)
LocalCache.consume(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 if (!isWriteable()) return
// val repliesToHex = listOfNotNull(replyingTo?.idHex).ifEmpty { null } // val repliesToHex = listOfNotNull(replyingTo?.idHex).ifEmpty { null }
@@ -549,13 +560,14 @@ class Account(
replyTos = repliesToHex, replyTos = repliesToHex,
mentions = mentionsHex, mentions = mentionsHex,
zapReceiver = zapReceiver, zapReceiver = zapReceiver,
markAsSensitive = wantsToMarkAsSensitive,
privateKey = loggedIn.privKey!! privateKey = loggedIn.privKey!!
) )
Client.send(signedEvent) Client.send(signedEvent)
LocalCache.consume(signedEvent, null) 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 if (!isWriteable()) return
val user = LocalCache.users[toUser] ?: return val user = LocalCache.users[toUser] ?: return
@@ -569,6 +581,7 @@ class Account(
replyTos = repliesToHex, replyTos = repliesToHex,
mentions = mentionsHex, mentions = mentionsHex,
zapReceiver = zapReceiver, zapReceiver = zapReceiver,
markAsSensitive = wantsToMarkAsSensitive,
privateKey = loggedIn.privKey!!, privateKey = loggedIn.privKey!!,
advertiseNip18 = false advertiseNip18 = false
) )
@@ -1114,6 +1127,12 @@ class Account(
saveable.invalidateData() saveable.invalidateData()
} }
fun updateShowSensitiveContent(show: Boolean?) {
showSensitiveContent = show
saveable.invalidateData()
live.invalidateData()
}
fun registerObservers() { fun registerObservers() {
// Observes relays to restart connections // Observes relays to restart connections
userProfile().live().relays.observeForever { userProfile().live().relays.observeForever {
@@ -22,7 +22,16 @@ class ChannelMessageEvent(
companion object { companion object {
const val kind = 42 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 content = message
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val tags = mutableListOf( val tags = mutableListOf(
@@ -37,6 +46,9 @@ class ChannelMessageEvent(
zapReceiver?.let { zapReceiver?.let {
tags.add(listOf("zap", it)) tags.add(listOf("zap", it))
} }
if (markAsSensitive) {
tags.add(listOf("content-warning", ""))
}
val id = generateId(pubKey, createdAt, kind, tags, content) val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = Utils.sign(id, privateKey) 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] } 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) override fun zapAddress() = tags.firstOrNull { it.size > 1 && it[0] == "zap" }?.get(1)
fun taggedAddresses() = tags.filter { it.size > 1 && it[0] == "a" }.mapNotNull { fun taggedAddresses() = tags.filter { it.size > 1 && it[0] == "a" }.mapNotNull {
@@ -37,4 +37,5 @@ interface EventInterface {
fun getPoWRank(): Int fun getPoWRank(): Int
fun zapAddress(): String? fun zapAddress(): String?
fun isSensitive(): Boolean
} }
@@ -51,7 +51,8 @@ class PollNoteEvent(
valueMinimum: Int?, valueMinimum: Int?,
consensusThreshold: Int?, consensusThreshold: Int?,
closedAt: Int?, closedAt: Int?,
zapReceiver: String? zapReceiver: String?,
markAsSensitive: Boolean
): PollNoteEvent { ): PollNoteEvent {
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val tags = mutableListOf<List<String>>() val tags = mutableListOf<List<String>>()
@@ -75,6 +76,9 @@ class PollNoteEvent(
if (zapReceiver != null) { if (zapReceiver != null) {
tags.add(listOf("zap", zapReceiver)) tags.add(listOf("zap", zapReceiver))
} }
if (markAsSensitive) {
tags.add(listOf("content-warning", ""))
}
val id = generateId(pubKey, createdAt, kind, tags, msg) val id = generateId(pubKey, createdAt, kind, tags, msg)
val sig = Utils.sign(id, privateKey) val sig = Utils.sign(id, privateKey)
@@ -73,7 +73,8 @@ class PrivateDmEvent(
privateKey: ByteArray, privateKey: ByteArray,
createdAt: Long = Date().time / 1000, createdAt: Long = Date().time / 1000,
publishedRecipientPubKey: ByteArray? = null, publishedRecipientPubKey: ByteArray? = null,
advertiseNip18: Boolean = true advertiseNip18: Boolean = true,
markAsSensitive: Boolean
): PrivateDmEvent { ): PrivateDmEvent {
val content = Utils.encrypt( val content = Utils.encrypt(
if (advertiseNip18) { nip18Advertisement } else { "" } + msg, if (advertiseNip18) { nip18Advertisement } else { "" } + msg,
@@ -94,6 +95,9 @@ class PrivateDmEvent(
zapReceiver?.let { zapReceiver?.let {
tags.add(listOf("zap", it)) tags.add(listOf("zap", it))
} }
if (markAsSensitive) {
tags.add(listOf("content-warning", ""))
}
val id = generateId(pubKey, createdAt, kind, tags, content) val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = Utils.sign(id, privateKey) val sig = Utils.sign(id, privateKey)
return PrivateDmEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) return PrivateDmEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
@@ -29,6 +29,7 @@ class TextNoteEvent(
addresses: List<ATag>?, addresses: List<ATag>?,
extraTags: List<String>?, extraTags: List<String>?,
zapReceiver: String?, zapReceiver: String?,
markAsSensitive: Boolean,
privateKey: ByteArray, privateKey: ByteArray,
createdAt: Long = Date().time / 1000 createdAt: Long = Date().time / 1000
): TextNoteEvent { ): TextNoteEvent {
@@ -56,6 +57,9 @@ class TextNoteEvent(
findURLs(msg).forEach { findURLs(msg).forEach {
tags.add(listOf("r", it)) tags.add(listOf("r", it))
} }
if (markAsSensitive) {
tags.add(listOf("content-warning", ""))
}
val id = generateId(pubKey, createdAt, kind, tags, msg) val id = generateId(pubKey, createdAt, kind, tags, msg)
val sig = Utils.sign(id, privateKey) 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.Bolt
import androidx.compose.material.icons.filled.Cancel import androidx.compose.material.icons.filled.Cancel
import androidx.compose.material.icons.filled.CurrencyBitcoin 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.ArrowForwardIos
import androidx.compose.material.icons.outlined.Bolt import androidx.compose.material.icons.outlined.Bolt
import androidx.compose.material.icons.rounded.Warning
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
@@ -83,7 +87,7 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
val keyboardController = LocalSoftwareKeyboardController.current val keyboardController = LocalSoftwareKeyboardController.current
val scroolState = rememberScrollState() val scrollState = rememberScrollState()
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
@@ -153,7 +157,7 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.verticalScroll(scroolState) .verticalScroll(scrollState)
) { ) {
Notifying(postViewModel.mentions) { Notifying(postViewModel.mentions) {
postViewModel.removeFromReplyList(it) postViewModel.removeFromReplyList(it)
@@ -335,10 +339,11 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n
} }
if (postViewModel.canUsePoll) { 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) { AddPollButton(postViewModel.wantsPoll) {
postViewModel.wantsPoll = !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) { ForwardZapTo(postViewModel) {
postViewModel.wantsForwardZapTo = !postViewModel.wantsForwardZapTo 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 @Composable
fun CloseButton(onCancel: () -> Unit) { fun CloseButton(onCancel: () -> Unit) {
Button( Button(
@@ -67,6 +67,9 @@ open class NewPostViewModel : ViewModel() {
var forwardZapTo by mutableStateOf<User?>(null) var forwardZapTo by mutableStateOf<User?>(null)
var forwardZapToEditting by mutableStateOf(TextFieldValue("")) var forwardZapToEditting by mutableStateOf(TextFieldValue(""))
// NSFW, Sensitive
var wantsToMarkAsSensitive by mutableStateOf(false)
open fun load(account: Account, replyingTo: Note?, quote: Note?) { open fun load(account: Account, replyingTo: Note?, quote: Note?) {
originalNote = replyingTo originalNote = replyingTo
replyingTo?.let { replyNote -> replyingTo?.let { replyNote ->
@@ -97,6 +100,7 @@ open class NewPostViewModel : ViewModel() {
contentToAddUrl = null contentToAddUrl = null
wantsForwardZapTo = false wantsForwardZapTo = false
wantsToMarkAsSensitive = false
forwardZapTo = null forwardZapTo = null
forwardZapToEditting = TextFieldValue("") forwardZapToEditting = TextFieldValue("")
@@ -118,13 +122,13 @@ open class NewPostViewModel : ViewModel() {
} }
if (wantsPoll) { 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) { } 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) { } 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 { } else {
account?.sendPost(tagger.message, tagger.replyTos, tagger.mentions, null, zapReceiver) account?.sendPost(tagger.message, tagger.replyTos, tagger.mentions, null, zapReceiver, wantsToMarkAsSensitive)
} }
cancel() cancel()
@@ -183,6 +187,7 @@ open class NewPostViewModel : ViewModel() {
wantsInvoice = false wantsInvoice = false
wantsForwardZapTo = false wantsForwardZapTo = false
wantsToMarkAsSensitive = false
forwardZapTo = null forwardZapTo = null
forwardZapToEditting = TextFieldValue("") 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) { if (newValue != null) {
account?.sendPost( account?.sendPost(
newValue.toString(), message = newValue.toString(),
listOfNotNull(bounty), replyTo = listOfNotNull(bounty),
listOfNotNull(bounty?.author), mentions = listOfNotNull(bounty?.author),
listOf("bounty-added-reward") tags = listOf("bounty-added-reward"),
wantsToMarkAsSensitive = false
) )
nextAmount = TextFieldValue("") 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.ResizeImage
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage 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.TranslatableRichTextViewer
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -369,15 +370,21 @@ private fun RenderRegularTextNote(
val modifier = remember { Modifier.padding(top = 5.dp) } val modifier = remember { Modifier.padding(top = 5.dp) }
if (eventContent != null) { if (eventContent != null) {
TranslatableRichTextViewer( val hasSensitiveContent = remember(note.event) { note.event?.isSensitive() ?: false }
content = eventContent, SensitivityWarning(
canPreview = canPreview, hasSensitiveContent = hasSensitiveContent,
modifier = modifier, accountViewModel = accountViewModel
tags = tags, ) {
backgroundColor = backgroundBubbleColor, TranslatableRichTextViewer(
accountViewModel = accountViewModel, content = eventContent,
nav = nav canPreview = canPreview,
) modifier = modifier,
tags = tags,
backgroundColor = backgroundBubbleColor,
accountViewModel = accountViewModel,
nav = nav
)
}
} else { } else {
TranslatableRichTextViewer( TranslatableRichTextViewer(
content = stringResource(id = R.string.could_not_decrypt_the_message), 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.RobohashAsyncImage
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage 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.TranslatableRichTextViewer
import com.vitorpamplona.amethyst.ui.components.VideoView import com.vitorpamplona.amethyst.ui.components.VideoView
import com.vitorpamplona.amethyst.ui.components.ZoomableContent import com.vitorpamplona.amethyst.ui.components.ZoomableContent
@@ -166,6 +167,7 @@ fun NoteCompose(
val noteEvent = remember(noteState) { note.event } val noteEvent = remember(noteState) { note.event }
val baseChannel = remember(noteState) { note.channel() } val baseChannel = remember(noteState) { note.channel() }
val isSensitive = remember(noteState) { note.event?.isSensitive() ?: false }
var popupExpanded by remember { mutableStateOf(false) } var popupExpanded by remember { mutableStateOf(false) }
@@ -183,7 +185,7 @@ fun NoteCompose(
note.let { note.let {
NoteQuickActionMenu(it, popupExpanded, { popupExpanded = false }, accountViewModel) NoteQuickActionMenu(it, popupExpanded, { popupExpanded = false }, accountViewModel)
} }
} else if (account.isHidden(noteForReports.author!!)) { } else if (account.isHidden(noteForReports.author!!) || (isSensitive && account.showSensitiveContent == false)) {
// Does nothing // Does nothing
} else { } else {
var showHiddenNote by remember { mutableStateOf(false) } var showHiddenNote by remember { mutableStateOf(false) }
@@ -435,13 +437,10 @@ private fun RenderTextEvent(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit nav: (String) -> Unit
) { ) {
val tags = remember(note.event?.id()) { note.event?.tags() } val eventContent = remember(note.event) { accountViewModel.decrypt(note) }
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) }
if (eventContent != null) { if (eventContent != null) {
val isAuthorTheLoggedUser = remember(note.event) { accountViewModel.isLoggedUser(note.author) }
if (makeItShort && isAuthorTheLoggedUser) { if (makeItShort && isAuthorTheLoggedUser) {
Text( Text(
text = eventContent, text = eventContent,
@@ -450,16 +449,27 @@ private fun RenderTextEvent(
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis
) )
} else { } else {
TranslatableRichTextViewer( val hasSensitiveContent = remember(note.event) { note.event?.isSensitive() ?: false }
content = eventContent,
canPreview = canPreview && !makeItShort,
modifier = modifier,
tags = tags,
backgroundColor = backgroundColor,
accountViewModel = accountViewModel,
nav = nav
)
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) DisplayUncitedHashtags(hashtags, eventContent, nav)
} }
} }
@@ -494,25 +504,31 @@ private fun RenderPoll(
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis
) )
} else { } else {
TranslatableRichTextViewer( val hasSensitiveContent = remember(note.event) { note.event?.isSensitive() ?: false }
eventContent, SensitivityWarning(
canPreview = canPreview && !makeItShort, hasSensitiveContent = hasSensitiveContent,
Modifier.fillMaxWidth(), accountViewModel = accountViewModel
noteEvent.tags(), ) {
backgroundColor, TranslatableRichTextViewer(
accountViewModel, eventContent,
nav canPreview = canPreview && !makeItShort,
) Modifier.fillMaxWidth(),
noteEvent.tags(),
backgroundColor,
accountViewModel,
nav
)
PollNote(
note,
canPreview = canPreview && !makeItShort,
backgroundColor,
accountViewModel,
nav
)
}
DisplayUncitedHashtags(noteEvent.hashtags(), eventContent, nav) DisplayUncitedHashtags(noteEvent.hashtags(), eventContent, nav)
PollNote(
note,
canPreview = canPreview && !makeItShort,
backgroundColor,
accountViewModel,
nav
)
} }
if (!makeItShort) { if (!makeItShort) {
@@ -586,15 +602,21 @@ private fun RenderPrivateMessage(
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis
) )
} else { } else {
TranslatableRichTextViewer( val hasSensitiveContent = remember(note.event) { note.event?.isSensitive() ?: false }
content = eventContent, SensitivityWarning(
canPreview = canPreview && !makeItShort, hasSensitiveContent = hasSensitiveContent,
modifier = modifier, accountViewModel = accountViewModel
tags = tags, ) {
backgroundColor = backgroundColor, TranslatableRichTextViewer(
accountViewModel = accountViewModel, content = eventContent,
nav = nav canPreview = canPreview && !makeItShort,
) modifier = modifier,
tags = tags,
backgroundColor = backgroundColor,
accountViewModel = accountViewModel,
nav = nav
)
}
DisplayUncitedHashtags(hashtags, eventContent, nav) DisplayUncitedHashtags(hashtags, eventContent, nav)
} }
@@ -2124,7 +2146,9 @@ fun UserPicture(
} }
val myIconModifier = remember { 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) { Box(myIconBoxModifier, contentAlignment = Alignment.Center) {
@@ -2147,7 +2171,9 @@ data class DropDownParams(
val isFollowingAuthor: Boolean, val isFollowingAuthor: Boolean,
val isPrivateBookmarkNote: Boolean, val isPrivateBookmarkNote: Boolean,
val isPublicBookmarkNote: Boolean, val isPublicBookmarkNote: Boolean,
val isLoggedUser: Boolean val isLoggedUser: Boolean,
val isSensitive: Boolean,
val showSensitiveContent: Boolean?
) )
@Composable @Composable
@@ -2158,20 +2184,23 @@ fun NoteDropDownMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Unit,
var reportDialogShowing by remember { mutableStateOf(false) } var reportDialogShowing by remember { mutableStateOf(false) }
val bookmarkState by accountViewModel.userProfile().live().bookmarks.observeAsState() val bookmarkState by accountViewModel.userProfile().live().bookmarks.observeAsState()
val accountState by accountViewModel.accountLiveData.observeAsState()
var state by remember { var state by remember {
mutableStateOf<DropDownParams>( 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) { withContext(Dispatchers.IO) {
state = DropDownParams( state = DropDownParams(
accountViewModel.isFollowing(note.author), isFollowingAuthor = accountViewModel.isFollowing(note.author),
accountViewModel.isInPrivateBookmarks(note), isPrivateBookmarkNote = accountViewModel.isInPrivateBookmarks(note),
accountViewModel.isInPublicBookmarks(note), isPublicBookmarkNote = accountViewModel.isInPublicBookmarks(note),
accountViewModel.isLoggedUser(note.author) 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") 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) { if (reportDialogShowing) {
@@ -51,7 +51,9 @@ fun PollNote(
val accountState by accountViewModel.accountLiveData.observeAsState() val accountState by accountViewModel.accountLiveData.observeAsState()
val account = remember(accountState) { accountState?.account } ?: return val account = remember(accountState) { accountState?.account } ?: return
val pollViewModel: PollNoteViewModel = viewModel() val pollViewModel: PollNoteViewModel = viewModel(
key = baseNote.idHex
)
LaunchedEffect(key1 = baseNote) { LaunchedEffect(key1 = baseNote) {
pollViewModel.load(account, 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.PinListEvent
import com.vitorpamplona.amethyst.service.model.PollNoteEvent import com.vitorpamplona.amethyst.service.model.PollNoteEvent
import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status 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.components.TranslatableRichTextViewer
import com.vitorpamplona.amethyst.ui.note.* import com.vitorpamplona.amethyst.ui.note.*
import com.vitorpamplona.amethyst.ui.note.BadgeDisplay import com.vitorpamplona.amethyst.ui.note.BadgeDisplay
@@ -376,15 +377,22 @@ fun NoteMaster(
!noteForReports.hasAnyReports() !noteForReports.hasAnyReports()
if (eventContent != null) { if (eventContent != null) {
TranslatableRichTextViewer( val hasSensitiveContent = remember(note.event) { note.event?.isSensitive() ?: false }
eventContent,
canPreview, SensitivityWarning(
Modifier.fillMaxWidth(), hasSensitiveContent = hasSensitiveContent,
note.event?.tags(), accountViewModel = accountViewModel
MaterialTheme.colors.background, ) {
accountViewModel, TranslatableRichTextViewer(
nav eventContent,
) canPreview,
Modifier.fillMaxWidth(),
note.event?.tags(),
MaterialTheme.colors.background,
accountViewModel,
nav
)
}
DisplayUncitedHashtags(noteEvent.hashtags(), eventContent, nav) DisplayUncitedHashtags(noteEvent.hashtags(), eventContent, nav)
@@ -232,6 +232,18 @@ class AccountViewModel(private val account: Account) : ViewModel() {
account.setHideBlockAlertDialog() account.setHideBlockAlertDialog()
} }
fun hideSensitiveContent() {
account.updateShowSensitiveContent(false)
}
fun disableContentWarnings() {
account.updateShowSensitiveContent(true)
}
fun seeContentWarnings() {
account.updateShowSensitiveContent(null)
}
class Factory(val account: Account) : ViewModelProvider.Factory { class Factory(val account: Account) : ViewModelProvider.Factory {
override fun <AccountViewModel : ViewModel> create(modelClass: Class<AccountViewModel>): AccountViewModel { override fun <AccountViewModel : ViewModel> create(modelClass: Class<AccountViewModel>): AccountViewModel {
return AccountViewModel(account) as AccountViewModel return AccountViewModel(account) as AccountViewModel
@@ -218,7 +218,7 @@ fun ChannelScreen(
onPost = { onPost = {
val tagger = NewMessageTagger(channel, listOfNotNull(replyTo.value?.author), listOfNotNull(replyTo.value), channelScreenModel.message.text) val tagger = NewMessageTagger(channel, listOfNotNull(replyTo.value?.author), listOfNotNull(replyTo.value), channelScreenModel.message.text)
tagger.run() 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("") channelScreenModel.message = TextFieldValue("")
replyTo.value = null replyTo.value = null
feedViewModel.invalidateData() // Don't wait a full second before updating feedViewModel.invalidateData() // Don't wait a full second before updating
@@ -178,7 +178,7 @@ fun ChatroomScreen(userId: String?, accountViewModel: AccountViewModel, nav: (St
trailingIcon = { trailingIcon = {
PostButton( PostButton(
onPost = { onPost = {
account.sendPrivateMessage(chatRoomScreenModel.message.text, userId, replyTo.value, null) account.sendPrivateMessage(chatRoomScreenModel.message.text, userId, replyTo.value, null, wantsToMarkAsSensitive = false)
chatRoomScreenModel.message = TextFieldValue("") chatRoomScreenModel.message = TextFieldValue("")
replyTo.value = null replyTo.value = null
feedViewModel.invalidateData() // Don't wait a full second before updating 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="channel_list_join_channel">Join</string>
<string name="today">Today</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> </resources>