mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-05 22:34:38 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bd4a450336 | ||
|
|
39818268ab | ||
|
|
d26b83a851 | ||
|
|
8dc9c2ed2f | ||
|
|
860f3bd096 | ||
|
|
a3adadb251 | ||
|
|
755d89dc79 | ||
|
|
fa4463f1d7 | ||
|
|
56a68b9100 | ||
|
|
fe317964b8 | ||
|
|
07ccf75d44 |
Generated
+1
-1
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="KotlinJpsPluginSettings">
|
||||
<option name="version" value="1.8.10" />
|
||||
<option name="version" value="1.8.21" />
|
||||
</component>
|
||||
</project>
|
||||
+6
-5
@@ -13,8 +13,8 @@ android {
|
||||
applicationId "com.vitorpamplona.amethyst"
|
||||
minSdk 26
|
||||
targetSdk 33
|
||||
versionCode 177
|
||||
versionName "0.51.2"
|
||||
versionCode 180
|
||||
versionName "0.52.0"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
@@ -24,6 +24,7 @@ android {
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
// TODO: Make sure all of JSON parsers work when activating these.
|
||||
//minifyEnabled true
|
||||
//proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
resValue "string", "app_name", "@string/app_name_release"
|
||||
@@ -71,7 +72,7 @@ android {
|
||||
}
|
||||
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion "1.4.3"
|
||||
kotlinCompilerExtensionVersion "1.4.7"
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
@@ -86,7 +87,7 @@ android {
|
||||
|
||||
dependencies {
|
||||
implementation 'androidx.core:core-ktx:1.10.1'
|
||||
implementation 'androidx.activity:activity-compose:1.7.1'
|
||||
implementation 'androidx.activity:activity-compose:1.7.2'
|
||||
implementation "androidx.compose.ui:ui:$compose_ui_version"
|
||||
implementation "androidx.compose.ui:ui-tooling-preview:$compose_ui_version"
|
||||
|
||||
@@ -131,7 +132,7 @@ dependencies {
|
||||
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
|
||||
|
||||
// link preview
|
||||
implementation 'org.jsoup:jsoup:1.13.1'
|
||||
implementation 'org.jsoup:jsoup:1.16.1'
|
||||
//implementation 'tw.com.oneup.www:Baha-UrlPreview:1.0.1'
|
||||
|
||||
// Encrypted Key Storage
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -39,7 +39,7 @@ class PrivateDmEvent(
|
||||
|
||||
fun with(pubkeyHex: String): Boolean {
|
||||
return pubkeyHex == pubKey ||
|
||||
tags.firstOrNull { it.size > 1 && it[0] == "p" }?.getOrNull(1) == pubkeyHex
|
||||
tags.any { it.size > 1 && it[0] == "p" && it[1] == pubkeyHex }
|
||||
}
|
||||
|
||||
fun plainContent(privKey: ByteArray, pubKey: ByteArray): String? {
|
||||
@@ -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),
|
||||
|
||||
@@ -35,9 +35,14 @@ import kotlinx.coroutines.launch
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun MessageSetCompose(messageSetCard: MessageSetCard, routeForLastRead: String, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
val noteState by messageSetCard.note.live().metadata.observeAsState()
|
||||
val baseNote = remember { messageSetCard.note }
|
||||
|
||||
val noteState by baseNote.live().metadata.observeAsState()
|
||||
val note = remember(noteState) { noteState?.note }
|
||||
|
||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||
val loggedIn = remember(accountState) { accountState?.account?.userProfile() } ?: return
|
||||
|
||||
var popupExpanded by remember { mutableStateOf(false) }
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
@@ -78,8 +83,8 @@ fun MessageSetCompose(messageSetCard: MessageSetCard, routeForLastRead: String,
|
||||
onClick = {
|
||||
scope.launch {
|
||||
routeFor(
|
||||
note,
|
||||
accountViewModel.userProfile()
|
||||
baseNote,
|
||||
loggedIn
|
||||
)?.let { nav(it) }
|
||||
}
|
||||
},
|
||||
@@ -90,18 +95,11 @@ fun MessageSetCompose(messageSetCard: MessageSetCard, routeForLastRead: String,
|
||||
|
||||
Column(columnModifier) {
|
||||
Row(Modifier.fillMaxWidth()) {
|
||||
Box(modifier = remember { Modifier.width(55.dp).padding(top = 5.dp, end = 5.dp) }) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_dm),
|
||||
null,
|
||||
modifier = remember { Modifier.size(16.dp).align(Alignment.TopEnd) },
|
||||
tint = MaterialTheme.colors.primary
|
||||
)
|
||||
}
|
||||
MessageIcon()
|
||||
|
||||
Column(modifier = remember { Modifier.padding(start = 10.dp) }) {
|
||||
NoteCompose(
|
||||
baseNote = messageSetCard.note,
|
||||
baseNote = baseNote,
|
||||
routeForLastRead = null,
|
||||
isBoostedNote = true,
|
||||
addMarginTop = false,
|
||||
@@ -116,3 +114,25 @@ fun MessageSetCompose(messageSetCard: MessageSetCard, routeForLastRead: String,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MessageIcon() {
|
||||
Box(
|
||||
modifier = remember {
|
||||
Modifier
|
||||
.width(55.dp)
|
||||
.padding(top = 5.dp, end = 5.dp)
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_dm),
|
||||
null,
|
||||
modifier = remember {
|
||||
Modifier
|
||||
.size(16.dp)
|
||||
.align(Alignment.TopEnd)
|
||||
},
|
||||
tint = MaterialTheme.colors.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,10 +8,12 @@ import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
@@ -28,6 +30,7 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.compositeOver
|
||||
import androidx.compose.ui.res.painterResource
|
||||
@@ -237,7 +240,7 @@ private fun RenderBoostGallery(
|
||||
null,
|
||||
modifier = remember {
|
||||
Modifier
|
||||
.size(18.dp)
|
||||
.size(19.dp)
|
||||
.align(Alignment.TopEnd)
|
||||
},
|
||||
tint = Color.Unspecified
|
||||
@@ -352,18 +355,21 @@ private fun AuthorPictureAndComment(
|
||||
)
|
||||
|
||||
amount?.let {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(MaterialTheme.colors.background.copy(0.52f)),
|
||||
contentAlignment = Alignment.BottomCenter
|
||||
) {
|
||||
Text(
|
||||
text = it,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = BitcoinOrange,
|
||||
fontSize = 12.sp
|
||||
)
|
||||
Box(modifier = Modifier.fillMaxSize().clip(shape = CircleShape), contentAlignment = Alignment.BottomCenter) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(MaterialTheme.colors.background.copy(0.62f)),
|
||||
contentAlignment = Alignment.BottomCenter
|
||||
) {
|
||||
Text(
|
||||
text = it,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colors.secondaryVariant,
|
||||
fontSize = 12.sp,
|
||||
modifier = Modifier.padding(bottom = 1.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
@@ -567,13 +583,18 @@ private fun RenderPrivateMessage(
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val noteEvent = note.event as? PrivateDmEvent ?: return
|
||||
|
||||
val withMe = remember { noteEvent.with(accountViewModel.userProfile().pubkeyHex) }
|
||||
val tags = remember(note.event?.id()) { note.event?.tags() }
|
||||
val hashtags = remember(note.event?.id()) { note.event?.hashtags() ?: emptyList() }
|
||||
val modifier = remember(note.event?.id()) { Modifier.fillMaxWidth() }
|
||||
val isAuthorTheLoggedUser = remember(note.event?.id()) { accountViewModel.isLoggedUser(note.author) }
|
||||
|
||||
if (withMe) {
|
||||
val eventContent = remember { accountViewModel.decrypt(note) }
|
||||
|
||||
if (eventContent != null) {
|
||||
if (makeItShort && accountViewModel.isLoggedUser(note.author)) {
|
||||
if (makeItShort && isAuthorTheLoggedUser) {
|
||||
Text(
|
||||
text = eventContent,
|
||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
|
||||
@@ -581,17 +602,23 @@ private fun RenderPrivateMessage(
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
} else {
|
||||
TranslatableRichTextViewer(
|
||||
content = eventContent,
|
||||
canPreview = canPreview && !makeItShort,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
tags = noteEvent.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(noteEvent.hashtags(), eventContent, nav)
|
||||
DisplayUncitedHashtags(hashtags, eventContent, nav)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -2119,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) {
|
||||
@@ -2142,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
|
||||
@@ -2153,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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -2244,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)
|
||||
|
||||
@@ -28,6 +28,7 @@ import androidx.compose.material.icons.outlined.Bolt
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -62,6 +63,7 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Composable
|
||||
@@ -124,6 +126,14 @@ fun ReplyReaction(
|
||||
val repliesState by baseNote.live().replies.observeAsState()
|
||||
val replies = remember(repliesState) { repliesState?.note?.replies } ?: emptySet()
|
||||
|
||||
val isWriteable = remember { accountViewModel.isWriteable() }
|
||||
|
||||
val replyCount by remember(repliesState) {
|
||||
derivedStateOf {
|
||||
showCount(replies.size)
|
||||
}
|
||||
}
|
||||
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
@@ -138,7 +148,7 @@ fun ReplyReaction(
|
||||
IconButton(
|
||||
modifier = iconButtonModifier,
|
||||
onClick = {
|
||||
if (accountViewModel.isWriteable()) {
|
||||
if (isWriteable) {
|
||||
onPress()
|
||||
} else {
|
||||
scope.launch {
|
||||
@@ -161,7 +171,7 @@ fun ReplyReaction(
|
||||
|
||||
if (showCounter) {
|
||||
Text(
|
||||
" ${showCount(replies.size)}",
|
||||
" $replyCount",
|
||||
fontSize = 14.sp,
|
||||
color = grayTint
|
||||
)
|
||||
@@ -179,11 +189,25 @@ fun BoostReaction(
|
||||
val boostsState by baseNote.live().boosts.observeAsState()
|
||||
val boostedNote = remember(boostsState) { boostsState?.note } ?: return
|
||||
|
||||
val hasBoosted = remember(boostsState) { accountViewModel.hasBoosted(baseNote) }
|
||||
val wasBoostedByLoggedIn = remember(boostsState) { boostedNote.isBoostedBy(accountViewModel.userProfile()) }
|
||||
val hasBoosted by remember(boostsState) {
|
||||
derivedStateOf {
|
||||
accountViewModel.hasBoosted(baseNote)
|
||||
}
|
||||
}
|
||||
|
||||
val wasBoostedByLoggedIn by remember(boostsState) {
|
||||
derivedStateOf {
|
||||
boostedNote.isBoostedBy(accountViewModel.userProfile())
|
||||
}
|
||||
}
|
||||
|
||||
val isWriteable = remember { accountViewModel.isWriteable() }
|
||||
|
||||
val boostCount = remember(boostsState) { showCount(boostedNote.boosts.size) }
|
||||
val boostCount by remember(boostsState) {
|
||||
derivedStateOf {
|
||||
showCount(boostedNote.boosts.size)
|
||||
}
|
||||
}
|
||||
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
@@ -258,11 +282,25 @@ fun LikeReaction(
|
||||
val reactionsState by baseNote.live().reactions.observeAsState()
|
||||
val reactedNote = remember(reactionsState) { reactionsState?.note } ?: return
|
||||
|
||||
val hasReacted = remember(reactionsState) { accountViewModel.hasReactedTo(baseNote) }
|
||||
val wasReactedByLoggedIn = remember(reactionsState) { reactedNote.isReactedBy(accountViewModel.userProfile()) }
|
||||
val hasReacted by remember(reactionsState) {
|
||||
derivedStateOf {
|
||||
accountViewModel.hasReactedTo(baseNote)
|
||||
}
|
||||
}
|
||||
|
||||
val wasReactedByLoggedIn by remember(reactionsState) {
|
||||
derivedStateOf {
|
||||
reactedNote.isReactedBy(accountViewModel.userProfile())
|
||||
}
|
||||
}
|
||||
|
||||
val isWriteable = remember { accountViewModel.isWriteable() }
|
||||
|
||||
val reactionCount = remember(reactionsState) { showCount(reactedNote.reactions.size) }
|
||||
val reactionCount by remember(reactionsState) {
|
||||
derivedStateOf {
|
||||
showCount(reactedNote.reactions.size)
|
||||
}
|
||||
}
|
||||
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
@@ -350,13 +388,22 @@ fun ZapReaction(
|
||||
LaunchedEffect(key1 = zapsState) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
if (!wasZappedByLoggedInUser) {
|
||||
wasZappedByLoggedInUser = accountViewModel.calculateIfNoteWasZappedByAccount(zappedNote)
|
||||
val newWasZapped = accountViewModel.calculateIfNoteWasZappedByAccount(zappedNote)
|
||||
|
||||
if (wasZappedByLoggedInUser != newWasZapped) {
|
||||
wasZappedByLoggedInUser = newWasZapped
|
||||
}
|
||||
}
|
||||
|
||||
zapAmountTxt = showAmount(account.calculateZappedAmount(zappedNote))
|
||||
val newZapAmount = showAmount(account.calculateZappedAmount(zappedNote))
|
||||
if (newZapAmount != zapAmountTxt) {
|
||||
zapAmountTxt = newZapAmount
|
||||
}
|
||||
|
||||
if (wasZappedByLoggedInUser) {
|
||||
zappingProgress = 1f
|
||||
if (abs(zappingProgress - 1) < 0.001) {
|
||||
zappingProgress = 1f
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ import com.vitorpamplona.amethyst.service.model.RepostEvent
|
||||
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.showAmountAxis
|
||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.ui.theme.RoyalBlue
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -285,7 +286,7 @@ fun UserReplyReaction(
|
||||
painter = painterResource(R.drawable.ic_comment),
|
||||
null,
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = Color.Cyan
|
||||
tint = RoyalBlue
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(10.dp))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -60,6 +60,8 @@ fun HomeScreen(
|
||||
HomeNewThreadFeedFilter.account = account
|
||||
HomeConversationsFeedFilter.account = account
|
||||
NostrHomeDataSource.invalidateFilters()
|
||||
homeFeedViewModel.invalidateData(true)
|
||||
repliesFeedViewModel.invalidateData(true)
|
||||
}
|
||||
|
||||
if (wantsToAddNip47 != null) {
|
||||
|
||||
+13
-11
@@ -52,6 +52,7 @@ import com.vitorpamplona.amethyst.ui.screen.CardFeedView
|
||||
import com.vitorpamplona.amethyst.ui.screen.NotificationViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.ScrollStateKeys
|
||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.ui.theme.RoyalBlue
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.math.BigDecimal
|
||||
@@ -69,19 +70,10 @@ fun NotificationScreen(
|
||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||
val account = remember(accountState) { accountState?.account } ?: return
|
||||
|
||||
if (scrollToTop) {
|
||||
val scope = rememberCoroutineScope()
|
||||
LaunchedEffect(key1 = Unit) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
notifFeedViewModel.clear()
|
||||
notifFeedViewModel.invalidateData(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(accountViewModel, account.defaultNotificationFollowList) {
|
||||
NostrAccountDataSource.invalidateFilters()
|
||||
NotificationFeedFilter.account = account
|
||||
notifFeedViewModel.invalidateData(true)
|
||||
}
|
||||
|
||||
val lifeCycleOwner = LocalLifecycleOwner.current
|
||||
@@ -99,6 +91,16 @@ fun NotificationScreen(
|
||||
}
|
||||
}
|
||||
|
||||
if (scrollToTop) {
|
||||
val scope = rememberCoroutineScope()
|
||||
LaunchedEffect(key1 = Unit) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
notifFeedViewModel.clear()
|
||||
notifFeedViewModel.invalidateData(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column(Modifier.fillMaxHeight()) {
|
||||
Column(
|
||||
modifier = Modifier.padding(vertical = 0.dp)
|
||||
@@ -130,7 +132,7 @@ fun SummaryBar(model: UserReactionsViewModel) {
|
||||
|
||||
val lineChartCount =
|
||||
lineChart(
|
||||
lines = listOf(Color.Cyan, Color.Green, Color.Red).map { lineChartColor ->
|
||||
lines = listOf(RoyalBlue, Color.Green, Color.Red).map { lineChartColor ->
|
||||
LineChart.LineSpec(
|
||||
lineColor = lineChartColor.toArgb(),
|
||||
lineBackgroundShader = DynamicShaders.fromBrush(
|
||||
|
||||
@@ -110,20 +110,11 @@ fun VideoScreen(
|
||||
NostrVideoDataSource.account = account
|
||||
VideoFeedFilter.account = account
|
||||
|
||||
if (scrollToTop) {
|
||||
val scope = rememberCoroutineScope()
|
||||
LaunchedEffect(key1 = Unit) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
NostrVideoDataSource.resetFilters()
|
||||
videoFeedView.invalidateData()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(accountViewModel, accountState.value?.account?.defaultStoriesFollowList) {
|
||||
VideoFeedFilter.account = account
|
||||
NostrVideoDataSource.account = account
|
||||
NostrVideoDataSource.resetFilters()
|
||||
videoFeedView.invalidateData()
|
||||
}
|
||||
|
||||
DisposableEffect(accountViewModel) {
|
||||
@@ -146,6 +137,16 @@ fun VideoScreen(
|
||||
}
|
||||
}
|
||||
|
||||
if (scrollToTop) {
|
||||
val scope = rememberCoroutineScope()
|
||||
LaunchedEffect(key1 = Unit) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
NostrVideoDataSource.resetFilters()
|
||||
videoFeedView.invalidateData()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column(Modifier.fillMaxHeight()) {
|
||||
Column(
|
||||
modifier = Modifier.padding(vertical = 0.dp)
|
||||
|
||||
@@ -7,6 +7,7 @@ val Purple500 = Color(0xFF6200EE)
|
||||
val Purple700 = Color(0xFF3700B3)
|
||||
val Teal200 = Color(0xFF03DAC5)
|
||||
val BitcoinOrange = Color(0xFFF7931A)
|
||||
val RoyalBlue = Color(0xFF4169E1)
|
||||
|
||||
val Following = Color(0xFF03DAC5)
|
||||
val Nip05 = Color(0xFF01BAFF)
|
||||
|
||||
@@ -7,19 +7,22 @@ import androidx.compose.material.darkColors
|
||||
import androidx.compose.material.lightColors
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
|
||||
private val DarkColorPalette = darkColors(
|
||||
primary = Purple200,
|
||||
primaryVariant = Purple700,
|
||||
secondary = Teal200
|
||||
secondary = Teal200,
|
||||
secondaryVariant = Color(0xFFF7931A)
|
||||
)
|
||||
|
||||
private val LightColorPalette = lightColors(
|
||||
primary = Purple500,
|
||||
primaryVariant = Purple700,
|
||||
secondary = Teal200
|
||||
secondary = Teal200,
|
||||
secondaryVariant = Color(0xFFB66605)
|
||||
|
||||
/* Other default colors to override
|
||||
background = Color.White,
|
||||
|
||||
@@ -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>
|
||||
|
||||
+4
-4
@@ -14,10 +14,10 @@ buildscript {
|
||||
}
|
||||
}// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
plugins {
|
||||
id 'com.android.application' version '8.0.1' apply false
|
||||
id 'com.android.library' version '8.0.1' apply false
|
||||
id 'org.jetbrains.kotlin.android' version '1.8.10' apply false
|
||||
id 'org.jetbrains.kotlin.jvm' version '1.8.10' apply false
|
||||
id 'com.android.application' version '8.0.2' apply false
|
||||
id 'com.android.library' version '8.0.2' apply false
|
||||
id 'org.jetbrains.kotlin.android' version '1.8.21' apply false
|
||||
id 'org.jetbrains.kotlin.jvm' version '1.8.21' apply false
|
||||
}
|
||||
|
||||
task installGitHook(type: Copy) {
|
||||
|
||||
Reference in New Issue
Block a user