diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 70735fdd8..0cd0018a2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -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.signers.EventTemplate 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.people.taggedUserIds import com.vitorpamplona.quartz.nip01Core.tags.references.references @@ -1292,6 +1293,21 @@ class Account( return event } + suspend fun signAnonymouslyAndBroadcast( + template: EventTemplate, + broadcast: List = 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. * Returns the event, target relays, and extra events to broadcast. diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/anonymous/AnonymousPostButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/anonymous/AnonymousPostButton.kt new file mode 100644 index 000000000..29a0f43ea --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/anonymous/AnonymousPostButton.kt @@ -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, + ) + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt index 603376d7f..b598a3459 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt @@ -25,7 +25,10 @@ import android.content.Intent import android.net.Uri import android.os.Parcelable import androidx.activity.compose.BackHandler +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -46,6 +49,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Switch +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect 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.theme.Size10dp 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.StdVertSpacer 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 if (postViewModel.voiceMetadata == null && postViewModel.voiceRecording == null) { Row( modifier = Modifier.padding(vertical = Size10dp), ) { - BaseUserPicture( - accountViewModel.userProfile(), - Size35dp, - accountViewModel = accountViewModel, - ) + if (postViewModel.wantsAnonymousPost) { + IconButton( + onClick = { postViewModel.wantsAnonymousPost = false }, + ) { + 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( R.string.what_s_on_your_mind, postViewModel, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt index c44877bc5..9b41af0b5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt @@ -285,6 +285,9 @@ open class ShortNotePostViewModel : var wantsZapRaiser by mutableStateOf(false) override val zapRaiserAmount = mutableStateOf(null) + // Anonymous Reply + var wantsAnonymousPost by mutableStateOf(false) + fun lnAddress(): String? = account.userProfile().lnAddress() fun hasLnAddress(): Boolean = account.userProfile().lnAddress() != null @@ -716,9 +719,12 @@ open class ShortNotePostViewModel : } val version = draftTag.current + val anonymous = wantsAnonymousPost cancel() - if (accountViewModel.settings.isCompleteUIMode()) { + if (anonymous) { + accountViewModel.account.signAnonymouslyAndBroadcast(template, extraNotesToBroadcast) + } else if (accountViewModel.settings.isCompleteUIMode()) { // Tracked broadcasting with progress feedback (non-blocking) val (event, relays, extras) = accountViewModel.account.createPostEvent(template, extraNotesToBroadcast) @@ -1078,6 +1084,7 @@ open class ShortNotePostViewModel : wantsToAddGeoHash = false wantsExclusiveGeoPost = false wantsSecretEmoji = false + wantsAnonymousPost = false forwardZapTo.value = SplitBuilder() forwardZapToEditting.value = TextFieldValue("") diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 28f63ffea..965229207 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -541,6 +541,10 @@ No trace in Nostr, only in Lightning + Anonymous + Post as a new throwaway identity. Your account will not be linked to this reply. + This reply will be posted from a new anonymous identity + File Server Choose a server to upload this file to