Merge pull request #1932 from vitorpamplona/claude/add-anonymous-reply-dkdSJ
feat: activate anonymous reply by tapping user picture
This commit is contained in:
@@ -139,6 +139,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
|||||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||||
import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate
|
import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate
|
||||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
||||||
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags
|
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags
|
||||||
import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUserIds
|
import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUserIds
|
||||||
import com.vitorpamplona.quartz.nip01Core.tags.references.references
|
import com.vitorpamplona.quartz.nip01Core.tags.references.references
|
||||||
@@ -1292,6 +1293,21 @@ class Account(
|
|||||||
return event
|
return event
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun <T : Event> signAnonymouslyAndBroadcast(
|
||||||
|
template: EventTemplate<T>,
|
||||||
|
broadcast: List<Event> = emptyList(),
|
||||||
|
): T {
|
||||||
|
val anonymousSigner = NostrSignerInternal(KeyPair())
|
||||||
|
val event = anonymousSigner.sign(template)
|
||||||
|
|
||||||
|
val relayList = nip65RelayList.outboxFlow.value.toSet()
|
||||||
|
|
||||||
|
client.send(event, relayList)
|
||||||
|
broadcast.forEach { client.send(it, relayList) }
|
||||||
|
|
||||||
|
return event
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a post event without sending it.
|
* Creates a post event without sending it.
|
||||||
* Returns the event, target relays, and extra events to broadcast.
|
* Returns the event, target relays, and extra events to broadcast.
|
||||||
|
|||||||
+58
@@ -0,0 +1,58 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.ui.note.creators.anonymous
|
||||||
|
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.PersonOff
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import com.vitorpamplona.amethyst.R
|
||||||
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.Size19Modifier
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AnonymousPostButton(
|
||||||
|
isActive: Boolean,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
) {
|
||||||
|
IconButton(
|
||||||
|
onClick = { onClick() },
|
||||||
|
) {
|
||||||
|
if (!isActive) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.PersonOff,
|
||||||
|
contentDescription = stringRes(R.string.post_anonymously),
|
||||||
|
modifier = Size19Modifier,
|
||||||
|
tint = MaterialTheme.colorScheme.onBackground,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.PersonOff,
|
||||||
|
contentDescription = stringRes(R.string.post_anonymously),
|
||||||
|
modifier = Size19Modifier,
|
||||||
|
tint = Color.Red,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+47
-5
@@ -25,7 +25,10 @@ import android.content.Intent
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.horizontalScroll
|
import androidx.compose.foundation.horizontalScroll
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
@@ -46,6 +49,7 @@ import androidx.compose.material3.MaterialTheme
|
|||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Switch
|
import androidx.compose.material3.Switch
|
||||||
|
import androidx.compose.material3.Text
|
||||||
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
|
||||||
@@ -106,6 +110,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsRow
|
|||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size19Modifier
|
import com.vitorpamplona.amethyst.ui.theme.Size19Modifier
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.Size35Modifier
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
||||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||||
import com.vitorpamplona.amethyst.ui.theme.SuggestionListDefaultHeightPage
|
import com.vitorpamplona.amethyst.ui.theme.SuggestionListDefaultHeightPage
|
||||||
@@ -283,16 +288,53 @@ private fun NewPostScreenBody(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (postViewModel.wantsAnonymousPost) {
|
||||||
|
Row(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(MaterialTheme.colorScheme.errorContainer)
|
||||||
|
.padding(horizontal = Size10dp, vertical = 8.dp),
|
||||||
|
verticalAlignment = CenterVertically,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringRes(R.string.anonymous_reply_warning),
|
||||||
|
color = MaterialTheme.colorScheme.onErrorContainer,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Only show text input if no voice message is being posted
|
// Only show text input if no voice message is being posted
|
||||||
if (postViewModel.voiceMetadata == null && postViewModel.voiceRecording == null) {
|
if (postViewModel.voiceMetadata == null && postViewModel.voiceRecording == null) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.padding(vertical = Size10dp),
|
modifier = Modifier.padding(vertical = Size10dp),
|
||||||
) {
|
) {
|
||||||
BaseUserPicture(
|
if (postViewModel.wantsAnonymousPost) {
|
||||||
accountViewModel.userProfile(),
|
IconButton(
|
||||||
Size35dp,
|
onClick = { postViewModel.wantsAnonymousPost = false },
|
||||||
accountViewModel = accountViewModel,
|
) {
|
||||||
)
|
Icon(
|
||||||
|
painter = painterRes(resourceId = R.drawable.incognito, 1),
|
||||||
|
contentDescription = stringRes(R.string.post_anonymously),
|
||||||
|
modifier = Size35Modifier,
|
||||||
|
tint = MaterialTheme.colorScheme.onBackground,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Box(
|
||||||
|
modifier =
|
||||||
|
Modifier.clickable {
|
||||||
|
postViewModel.wantsAnonymousPost = true
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
BaseUserPicture(
|
||||||
|
accountViewModel.userProfile(),
|
||||||
|
Size35dp,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
MessageField(
|
MessageField(
|
||||||
R.string.what_s_on_your_mind,
|
R.string.what_s_on_your_mind,
|
||||||
postViewModel,
|
postViewModel,
|
||||||
|
|||||||
+8
-1
@@ -285,6 +285,9 @@ open class ShortNotePostViewModel :
|
|||||||
var wantsZapRaiser by mutableStateOf(false)
|
var wantsZapRaiser by mutableStateOf(false)
|
||||||
override val zapRaiserAmount = mutableStateOf<Long?>(null)
|
override val zapRaiserAmount = mutableStateOf<Long?>(null)
|
||||||
|
|
||||||
|
// Anonymous Reply
|
||||||
|
var wantsAnonymousPost by mutableStateOf(false)
|
||||||
|
|
||||||
fun lnAddress(): String? = account.userProfile().lnAddress()
|
fun lnAddress(): String? = account.userProfile().lnAddress()
|
||||||
|
|
||||||
fun hasLnAddress(): Boolean = account.userProfile().lnAddress() != null
|
fun hasLnAddress(): Boolean = account.userProfile().lnAddress() != null
|
||||||
@@ -716,9 +719,12 @@ open class ShortNotePostViewModel :
|
|||||||
}
|
}
|
||||||
|
|
||||||
val version = draftTag.current
|
val version = draftTag.current
|
||||||
|
val anonymous = wantsAnonymousPost
|
||||||
cancel()
|
cancel()
|
||||||
|
|
||||||
if (accountViewModel.settings.isCompleteUIMode()) {
|
if (anonymous) {
|
||||||
|
accountViewModel.account.signAnonymouslyAndBroadcast(template, extraNotesToBroadcast)
|
||||||
|
} else if (accountViewModel.settings.isCompleteUIMode()) {
|
||||||
// Tracked broadcasting with progress feedback (non-blocking)
|
// Tracked broadcasting with progress feedback (non-blocking)
|
||||||
val (event, relays, extras) = accountViewModel.account.createPostEvent(template, extraNotesToBroadcast)
|
val (event, relays, extras) = accountViewModel.account.createPostEvent(template, extraNotesToBroadcast)
|
||||||
|
|
||||||
@@ -1078,6 +1084,7 @@ open class ShortNotePostViewModel :
|
|||||||
wantsToAddGeoHash = false
|
wantsToAddGeoHash = false
|
||||||
wantsExclusiveGeoPost = false
|
wantsExclusiveGeoPost = false
|
||||||
wantsSecretEmoji = false
|
wantsSecretEmoji = false
|
||||||
|
wantsAnonymousPost = false
|
||||||
|
|
||||||
forwardZapTo.value = SplitBuilder()
|
forwardZapTo.value = SplitBuilder()
|
||||||
forwardZapToEditting.value = TextFieldValue("")
|
forwardZapToEditting.value = TextFieldValue("")
|
||||||
|
|||||||
@@ -541,6 +541,10 @@
|
|||||||
<string name="zap_type_nonzap_explainer">No trace in Nostr, only in Lightning</string>
|
<string name="zap_type_nonzap_explainer">No trace in Nostr, only in Lightning</string>
|
||||||
|
|
||||||
|
|
||||||
|
<string name="post_anonymously">Anonymous</string>
|
||||||
|
<string name="post_anonymously_explainer">Post as a new throwaway identity. Your account will not be linked to this reply.</string>
|
||||||
|
<string name="anonymous_reply_warning">This reply will be posted from a new anonymous identity</string>
|
||||||
|
|
||||||
<string name="file_server">File Server</string>
|
<string name="file_server">File Server</string>
|
||||||
<string name="file_server_description">Choose a server to upload this file to</string>
|
<string name="file_server_description">Choose a server to upload this file to</string>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user